Java教程

Java泛型详解

本文主要是介绍Java泛型详解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

什么是泛型

泛型其实和模板的概念十分相似,泛型是一种技术,也可以理解为对泛型类和泛型接口的统称,在 ArrayList<T>() 中,T 称为类型参数 Type Parameter 也称为形式类型参数。当在使用泛型时例如实例化 ArrayList 时,new ArrayList<String>(); 中的 String 可以认为是对 T 的赋值操作,这里的 String 被称为实际类型参数 actual type parameter

还可以把泛型理解为:变量是对数据的抽取,而泛型是对变量类型的抽取,抽取成类型参数,抽象层次更高。

ArrayList

ArrayList 本质上是一个可以自动扩容的数组。

JDK1.5 引入泛型之前,ArrayList采取的方式是:在内部定义一个 Object[] array

public class ArrayList {
    private Object[] array;
    private int size;
    public void add(Object e) {...}
    public void remove(int index) {...}
    public Object get(int index) {...}
}

在使用这种没有泛型的解决方法时会经常出现两个问题

  • 需要强制转换类型
  • 容易发生 ClassCastException

于是在 JDK1.5 之前,可以单独为某个特定的 List 编写一个特定的 ArrayList

public class StringArrayList {
    // 因为这种ArrayList只存String,所以不需要用Object[]兼容所有类型,只要String[]即可
    private String[] array;
    private int size;
    public void add(String e) {...}
    public void remove(int index) {...}
    public String get(int index) {...}
}

此时,存入和取出的类型就被固定,不会因为强制类型转换而出现问题。但是,这种方法又带来一个全新的问题,需要使用 ArrayList 的对象不单单只有 String 还有 IntegerfloatLong 等等,而为每一个类型再独自编写一个特定的 ArrayList 又是不太现实的,所以就引入了泛型的概念。

java 中,泛型更像一种 “假泛型”因为 java 中的泛型只会存在源码阶段,只会将用户在源码指定的当前形式类型参数使用编译器来进行类型转换的限定和对传入的内容进行检查,在编译完成后依旧是使用 Object 进行储存,自此,使用泛型完美解决了 ArrayList 的两个缺点,也不会出现第二种代码重复量较大的问题。

public class ArrayList<T> {
    private T[] array;
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

image

泛型擦除与自动类型转换

泛型擦除是指 java 源码在编译为 class 之后并没有泛型这个概念,泛型只是 JDK 为编译器做的语法糖且只在编译期间存在。其实泛型的本质还是使用 Object 接收然后使用类型强转,而在实例化对象时需要在尖括号中写明的类名就是 Object 需要强转成的类型。

测试代码

  • BaseDao :实现泛型的类
public class BaseDao<T> {

    public T get(T t){
        return t;
    }

    public List<T> getList(T t){
        return new ArrayList<>();
    }
}
  • User :需要传入泛型中的类,空白即可
public class User {
}
  • UserDao :继承 BaseDao ,确定泛型
public class UserDao extends BaseDao<User>{
}
  • Test :测试类
public class Test {
    public static void main(String[] args) {
        UserDao userDao = new UserDao();
        User user = userDao.get(new User());
        System.out.println(user);
        List<User> list = userDao.getList(new User());
        System.out.println(list);
    }
}

测试代码完成后并没有任何问题,在 idea 中编译后查看 class 也没有任何变化,这是因为idea自带的反编译工具把泛型的代码又给反编译了回来,所以需要使用一个不那么智能的反编译工具,才可以看到泛型编译为字节码之后变成了什么。

javare-在线反编译

在使用以上反编译网站进行编译之后生成以下代码。

  • BaseDao
public class BaseDao {

   public Object get(Object t) {
      return t;
   }

   public List getList(Object t) {
      return new ArrayList();
   }
}
  • Test
public class Test {

   public static void main(String[] args) {
      UserDao userDao = new UserDao();
      User user = (User)userDao.get(new User());
      System.out.println(user);
      List list = userDao.getList(new User());
      System.out.println(list);
   }
}

可以看到,在反编译后的代码中完全没有 T 没有泛型,内容所有的参数和返回值都是使用 Object 类型。可以看到 User user = (User)userDao.get(new User()); 在调用方法返回的其实是 Object ,编译器自动帮我们添加了强转类型的代码这就是自动类型转换。而 List list = userDao.getList(new User()); 获取 list 的部分则是因为,获取的类就是 List 而泛型 User 只是 List 其中的属性,所以此处就去掉了尖括号中的泛型的内容,这就是泛型擦除。

泛型边界

在实例化一个 ArrayList 时,我们需要等号左右两侧的泛型是相等的,例如:List<String> list = new ArrayList<String>(); ,此时就可以简写方式省略掉等号右侧的泛型 new ArrayList<>(); 。当等号左右侧的泛型不同时,编译器则会报错。

image

如果现在我们需要有一个方法,用来把 list 输出打印一个日志,可以直接添加以下方法。

public static void print(List<String> list) {
    System.out.println(list.toString());
}

这种方法在传入 List<String> 对象时可以正常运行,但是如果需要传入 List<Integer> 时,编译器则会报错。当我们在指定 List 的泛型为 Object 时可以传入任何对象作为元素,如果我们将方法的参数修改为 List<Object> list 是否可以传入 StringInteger 的参数呢?

image

结果现在 StringInteger 都无法传入了,为什么可以在声明为 objectlist 中添加任意类型的元素,但是在传参时就不能这么做呢?

image

其实在传参时的情况和调用 listadd() 方法并不一样,在调用 add(item) 方法时相当于调用 public void add (Object o) ,此时相当于将 item 赋值给参数 Object o = item 。但是在直接传入 list 时情况就不同了,就变成了 List<Object> list = otherList

image

也可以看到上面的代码,刚才已经明确了等号左侧和右侧的泛型实际类型参数必须相等,但是在这里已经不同了。

但是如何让这个 list 可以接受实际类型参数为子类的 list 的赋值呢?这时就需要用到边界通配符了。

什么是通配符,通配符在哪里?

例如这行代码 List<Number> 其中,编译器会认为 Number 作为上边界类型,只可以将此类型和它的子类作为元素存入 List ,即 IntegerLong 等。

  • 创建测试类

image

  • 测试一下子类元素存放
List<Person> personList = new ArrayList<>();
personList.add(new Man("111"));
personList.add(new Women("222"));

List<Bird> birdList = new ArrayList<>();
birdList.add(new Eagle("xxx"));
birdList.add(new Vulture("yyy"));

可以看到上面的代码,实例化 list 时使用的实际类型参数是父类,而传入的元素则是子类。这种只是单单元素存放,我们在上面已经介绍了,其实就是 add 方法中的参数类型是 Number 时可以接受所有它的子类。下面是几种更加高级的通配符介绍,用来解决等号左右侧的实际类型参数必须相同的问题。

extends 上边界通配符

使用已经创建好的类来进行测试,需要测试三个方面

  • 可以直接赋值什么类型的 list
  • 取出元素后强转的类型
  • 可以存入什么元素

首先创建用来赋值的 list

// 动物list
List<Animal> animalList = new ArrayList<>();
animalList.add(new Bird("123"));
animalList.add(new Eagle("123"));
// 鸟类list
List<Bird> birdList = new ArrayList<>();
birdList.add(new Eagle("xxx"));
birdList.add(new Vulture("yyy"));
// 老鹰list
List<Eagle> eagleList = new ArrayList<>();
eagleList.add(new Eagle("111"));
eagleList.add(new Eagle("222"));

测试赋值操作

// 上边界为鸟类,可以直接赋值实际类型参数为此类或其子类的list
List<? extends Bird> exBirdList = eagleList;
List<? extends Bird> exBirdList1 = birdList;
List<? extends Bird> exBirdList2 = animalList; // ERROR

可以直接赋值此类或者子类的 list

测试取出元素

List<? extends Bird> exBirdList = eagleList;
// 取出的元素统一向上边界类强转
Bird bird = exBirdList.get(0);
List<? extends Bird> exBirdList1 = birdList;
Bird bird1 = exBirdList1.get(0);

无论赋值的类型是什么,统一向上边界的类型进行强转

测试存入元素

List<? extends Bird> exBirdList = eagleList;
// 在直接赋值list后,就不可以添加任何元素
exBirdList.add(new Bird("123")); // ERROR
exBirdList.add(new Eagle("123")); // ERROR
List<? extends Bird> exBirdList1 = birdList;
exBirdList1.add(new Bird("123")); // ERROR
exBirdList1.add(new Eagle("123")); // ERROR
exBirdList1.add("String"); // ERROR

无论赋值什么类型,都不可以添加任何元素

super 下边界通配符

测试赋值操作

List<? super Bird> superBirdList = birdList;
List<? super Bird> supserBirdList1 = eagleList; // ERROR
List<? super Bird> superBirdList2 = animalList;

只可以赋值实际类型参数为指定类型或指定类型的父类

测试取出元素

Object object = superBirdList.get(0);
Object object1 = superBirdList2.get(0);

直接取出 Object 不会强转

为什么只会取出 Object 类型的元素?在赋值时存入的内容是指定类和其父类,而 Object 类作为所有类的父类,下边界是指定的类型那上边界一定就是 Object 类,如果使用其他类 如 Animal 或者 Bird ,就会在下面直接赋值 Object 类型的 list 的情况下发生 ClassCastException

List<Object> objectList = new ArrayList<>();
objectList.add("string");
objectList.add(10);
objectList.add(10000L);
List<? super Bird> superBirdList3 = objectList;

测试存入元素

List<? super Bird> superBirdList = birdList;
List<? super Bird> superBirdList2 = animalList;

superBirdList.add(new Animal("123")); // ERROR
superBirdList.add(new Bird("123"));
superBirdList.add(new Eagle("123"));

superBirdList2.add(new Animal("123")); // ERROR
superBirdList2.add(new Bird("123"));
superBirdList2.add(new Eagle("123"));
superBirdList2.add("string"); // ERROR

只可以存入指定类及其的子类

总结

  • 上边界 extends
    • 赋值:指定类及其子类
    • 取出:指定类
    • 存入:不可存入
  • 下边界 super
    • 赋值:指定类及其父类
    • 取出:Object
    • 存入:指定类及其子类

泛型类与方法

介绍使用方法

泛型类

public class GenericFunctionTest<T, V> {}

使用单个大写字母定义类型变量,可用逗号分隔定义多个。

泛型方法

// 普通泛型方法
public <E> void basicFunction(E e){
    // code ...
}
// 返回值为泛型的方法
public <E> E hasReturnFunction(E e){
    // code ...
    return e;
}
// 静态泛型方法
public static <E> E hasReturnStaticFunction(E e){
    // code ...
    return e;
}

定义泛型的位置在返回值前,格式与泛型类定义格式相同。泛型方法定义的泛型作用范围在方法中,在一个类中有多个名为 E 的形式类型参数的方法,它们之间互不影响各自的作用域就是各自的方法,同样也可以为作为参数和返回值。

常用使用场景

  • 用于 list 的一些操作
/**
 * 将List转为Map
 *
 * @param list         原数据
 * @param keyExtractor Key的抽取规则
 * @param <K>          Key
 * @param <V>          Value
 * @return
 */
public static <K, V> Map<K, V> listToMap(List<V> list, Function<V, K> keyExtractor) {
    if (list == null || list.isEmpty()) {
        return new HashMap<>();
    }
    Map<K, V> map = new HashMap<>(list.size());
    for (V element : list) {
        K key = keyExtractor.apply(element);
        if (key == null) {
            continue;
        }
        map.put(key, element);
    }
    return map;
}
  • 接口返回值格式化
public static <T> Result<T> success(T data) {
    return new Result<>(ExceptionCodeEnum.SUCCESS.getCode(), ExceptionCodeEnum.SUCCESS.getDesc(), data);
}

参考引用

  • 能解答一切的答案——bravo1988
这篇关于Java泛型详解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!