前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java基础不简单,泛型很重要!

Java基础不简单,泛型很重要!

作者头像
java技术爱好者
发布2021-10-13 15:31:25
2160
发布2021-10-13 15:31:25
举报
文章被收录于专栏:java技术爱好者java技术爱好者

前言

其实在开发中经常会看到泛型的使用,但是很多人对其也是一知半解,大概知道这是一个类似标签的东西。比如最常见的给集合定义泛型。

代码语言:javascript
复制
List<String> list = new ArrayList<>();
Map<String,Object> map = new HashMap<>();

那么什么是泛型,为什么使用泛型,怎么使用泛型,接着往下看。

什么是泛型

Java泛型是J2SE1.5中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter),这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。-- 百度百科

这句话读起来有点拗口,但是我们要抓住他说的关键,参数化类型可以用在类、接口和方法的创建中,我们知道泛型是在什么地方使用。

为什么使用泛型

一般我在思考这种问题时,会反过来思考,假如没有泛型会怎么样?

我们以最简单的List集合为例子,假如没有泛型:

代码语言:javascript
复制
public static void main(String[] args) {
    List list = new ArrayList();
    list.add("good");
    list.add(100);
    list.add('a');
    for(int i = 0; i < list.size(); i++){
        String val = (String) list.get(i);
        System.out.println("val:" + val);
    }
}

很显然在没有泛型的时候,List默认是Object类型,所以List里的元素可以是任意的,看起来集合里装着任意类型的参数是“挺不错”,但是任意的类型的缺点也是很明显的,就是要开发者对集合中的元素类型在预知的情况下进行操作,否则编译时不会提示错误,但是运行时很容易出现类型转换异常(ClassCastException)。

如果没有泛型,第二个小问题是,我们把一个对象放进了集合中,但是集合并不会记住这个对象的类型,再次取出时统统都会变成Object类,但是在运行时仍然为其本身的类型。

所以引入泛型就可以解决以上两个问题:

  • 类型安全问题。使用泛型,则会在编译期就能发现类型转换异常的错误。
  • 消除类型强转。泛型可以消除源代码中的许多强转类型的操作,这样可以使代码更加可读,并减少出错的机会。

泛型的特性

泛型只有在编译阶段有效,在运行阶段会被擦除。

下面做个试验,请看代码:

代码语言:javascript
复制
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
System.out.println(classStringArrayList == classIntegerArrayList);

结果是true,由此可看出,在运行时ArrayList<String>ArrayList<Integer>都会被擦除成ArrayList

Java 泛型擦除是 Java 泛型中的一个重要特性,其目的是避免过多的创建类而造成的运行时的过度消耗。

泛型的使用方式

在上文也提到泛型有三种使用方式:泛型类、泛型接口、泛型方法。

泛型类

基本语法:

代码语言:javascript
复制
public class 类名<泛型标识, 泛型标识, ...> {
    private 泛型标识 变量名;
}

示例代码:

代码语言:javascript
复制
public class GenericClass<T> {
    private T t;
}

在泛型类里面,泛型形参T可用在返回值和方法参数上,例如:

代码语言:javascript
复制
public class GenericClass<T> {

    private T t;

    public GenericClass() {
    }

    public void setValue(T t) {//作为参数
        this.t = t;
    }

    public T getValue() {//作为返回值
        return t;
    }
}

当我们创建类实例时,就可以传入类型实参:

代码语言:javascript
复制
public static void main(String[] args) throws Exception {
    //泛型传入了String类型
    GenericClass<String> generic = new GenericClass<String>();
    //这里就限制了setValue()方法只能传入String类型
    generic.setValue("abc");
    //限制了getValue()方法得到的也是String类型
    String value = generic.getValue();
    System.out.println(value);
}

这里与普通类创建实例不同的地方在于,泛型类的构造需要在类名后面添加上<泛型>,这个尖括号传的是什么类型,T就代表什么类型。泛型类的作用就体现在这里,他限制了这个类的使用了泛型标识的方法的返回值和参数。

泛型方法

基本语法:

代码语言:javascript
复制
修饰符 <T, E, ...> 返回值类型 方法名(形参列表){
}

示例:

代码语言:javascript
复制
//静态方法
public static <E> E getObject1(Class<E> clazz) throws Exception {
    return clazz.newInstance();
}

//实例方法
public <E> E getObject2(Class<E> clazz) throws Exception {
    return clazz.newInstance();
}

使用示例:

代码语言:javascript
复制
public static void main(String[] args) throws Exception {
    GenericClass<String> generic = new GenericClass<>();
    Object object1 = GenericClass.getObject1(Object.class);
    System.out.println(object1);
    Object object2 = generic.getObject2(Object.class);
    System.out.println(object2);
}

其实泛型方法比较简单,就是在返回值前加上尖括号<泛型标识>来表示泛型变量。不过对于初学者来说,很容易会跟泛型类的泛型方法混淆,特别是泛型类里定义了泛型方法的情况。

代码语言:javascript
复制
public class GenericClass<T> {
    private T t;

    public GenericClass() {
    }
 //泛型类的方法
    public void setValue(T t) {
        this.t = t;
    }
 //泛型类的方法
    public T getValue() {
        return t;
    }
 //静态泛型方法,区别在于在返回值前需要加上尖括号<泛型标识>
    public static <E> E getObject1(Class<E> clazz) throws Exception {
        return clazz.newInstance();
    }
 //实例泛型方法,区别在于在返回值前需要加上尖括号<泛型标识>
    public <E> E getObject2(Class<E> clazz) throws Exception {
        return clazz.newInstance();
    }
}

泛型接口

基本语法:

代码语言:javascript
复制
public 接口名称 <泛型标识, 泛型标识, ...>{
    泛型标识 方法名();
}

示例:

代码语言:javascript
复制
public interface Generator<T> {
    T next();
}

当实现泛型接口不传入实参时,与泛型类定义相同,需要将泛型的声明加在类名后面:

代码语言:javascript
复制
public class ObjectGenerator<T> implements Generator<T> {
    @Override
    public T next() {
        return null;
    }
}

当实现泛型接口传入实参时,当泛型参数传入了具体的实参后,则所有使用到泛型的地方都会被替换成传入的参数类型。比如下面的例子中,next()方法的返回值就变成了Integer类型:

代码语言:javascript
复制
public class IntegerGenerator implements Generator<Integer> {
    @Override
    public Integer next() {
        Random random = new Random();
        return random.nextInt(10);
    }
}

泛型通配符

类型通配符一般是使用 ? 代替具体的类型实参,这里的 ? 是具体的类型实参,而不是类型形参。它跟Integer,Double,String这些类型一样都是一种实际的类型,我们可以把 ? 看作是所有类型的父类。

类型通配符又可以分为类型通配符上限和类型通配符下限。

类型通配符上限

基本语法:

代码语言:javascript
复制
类/接口<? extends 实参类型>

要求该泛型的类型,只能是实参类型,或实参类型的 子类 类型。

比如:

代码语言:javascript
复制
public class ListUtils<T extends List> {
    public void doing(T t) {
        System.out.println("size: " + t.size());
        for (Object o : t) {
            System.out.println(o);
        }
    }
}

这样就限制了传入的泛型只能是List的子类,如下所示:

代码语言:javascript
复制
public static void main(String[] args) throws Exception {
    ListUtils<List> utils = new ListUtils<>();
    List<String> list = Arrays.asList("1", "2", "3");
    utils.doing(list);
}

类型通配符下限

基本语法:

代码语言:javascript
复制
类/接口<? super 实参类型>

要求该泛型的类型,只能是实参类型,或实参类型的 父类 类型

比如:

代码语言:javascript
复制
public class ListUtils {
    //静态泛型方法,使用super关键字定义通配符下限
    public static <E> void copy(Collection<? super E> parentList, Collection<E> childList) {
        for (E e : childList) {
            parentList.add(e);
        }
    }
}

限制了传入的List类型:

代码语言:javascript
复制
public static void main(String[] args) throws Exception {
    Integer i1 = new Integer(10);
    Integer i2 = new Integer(20);
    List<Integer> integers = new ArrayList<>();
    integers.add(i1);
    integers.add(i2);
    List<Number> numbers = new ArrayList<>();
    //childList传入的是List<Integer>,所以parentList传入的只能是Integer本身或者其父类的List
    ListUtils.copy(numbers, integers);
    System.out.println(numbers);//[10, 20]
}

泛型使用注意点

不能在静态字段使用泛型。

不能和基本类型一起使用。

异常类不能使用泛型。

总结

最后总结一下泛型的作用:

  • 提高了代码的可读性,这点毋庸置疑。
  • 解决类型安全的问题,在编译期就能发现类型转换异常的错误。
  • 可拓展性强,可以使用泛型通配符定义,不需要定义实际的数据类型。
  • 提高了代码的重用性。可以重用数据处理算法,不需要为每一种类型都提供特定的代码。
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-10-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 java技术爱好者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 什么是泛型
  • 为什么使用泛型
  • 泛型的特性
  • 泛型的使用方式
    • 泛型类
      • 泛型方法
        • 泛型接口
          • 泛型通配符
            • 类型通配符上限
            • 类型通配符下限
          • 泛型使用注意点
          • 总结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档