栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

泛型【Java笔记】

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

泛型【Java笔记】

泛型

泛型(Generics)是一种“代码模板”,可以用一套代码套用各种类型。

设计背景

集合容器类在设计阶段/声明阶段,除了元素的类型不确定,其他的部分都是确定的(例如关于这个元素如何保存、如何管理等是确定的)。

由于不能确定这个容器到底实际存的是什么类型的对象,又不可能为每个类型单独设计一个集合容器类,所以在一开始只能把元素类型设计为Object,以使调用者可以传入任何类型的对象。

但这样做的弊端是:

  • 传入数据时,类型不安全,编译器无法检查传入的对象是否是单一类型(例如可能传入了一个String,后面又传入了一个Integer);
  • 读取数据时,需要进行强制类型转换,十分繁琐,而且有出现 ClassCastException 的风险。

JDK1.5 之后,Java 引入了泛型来解决这个问题。

什么是泛型

从 JDK1.5以后,Java引入了 “参数化类型(Parameterized type)” 的概念,允许我们在创建集合时再指定集合元素的类型(例如List,表明该List只能保存String类型的对象)。此时,这个类型参数叫做泛型(Collection, List, ArrayList 中的这个就是类型参数,即泛型)。

引入泛型后,成功解决了先前的弊端:

  • 传入数据时,只有指定类型才可以添加到集合中,类型安全;
  • 读取数据时,读取出来的对象不需要强转,操作更便捷;
  • 同时没有抛出 ClassCastException 的风险,因为泛型把运行时期的问题提前到了编译期间,让编译器来检查类型。

JDK1.5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、 创建集合对象时传入类型实参。而在集合内部,对应属性的类型就用E来表示(例如:E[] values)。

此时,这些集合类就变成了一种模板,一套代码套用各种类型。

综上,泛型机制,就是 允许在定义类、接口、方法时通过一个标识符/占位符表示类中某个属性的类型或者是某个方法的返回值及参数类型,等到使用时再确定这个类型具体是什么 的机制。

使用泛型

使用集合类如ArrayList时,如果不定义泛型类型时,编译器会自动把当作Object使用。此时,没有发挥泛型的优势,编译器也会发出警告:

// 编译器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);

当我们定义泛型类型后,List的泛型接口变为强类型List

// 无编译器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
// 无强制转型:
String first = list.get(0);
String second = list.get(1);

泛型的实例化:正常实例化的基础上,在类名后面指定类型参数的值(类型)。如:

List strList = new ArrayList();
Iterator iterator = customers.iterator();

JDK 1.7 后,编译器如果能自动推断出泛型类型,就可以省略后面的泛型类型。例如,对于下面的代码:

List list = new ArrayList();

编译器看到泛型类型List就可以自动推断出后面的ArrayList的泛型类型必须是ArrayList,因此,可以把代码简写为:

// 可以省略后面的Number,编译器可以自动推断泛型类型:
List list = new ArrayList<>();
自定义泛型结构

自定义范型结构,与定义一般的结构几乎没有区别。在定义时,只需要额外添加标识符,并在需要的地方将类型定义为T即可。

说明:

  • 此处 T 可以随便写为任意标识,常见的如T (Type)、E (Element)、K (Key)、V (Value) 等形式的参数常用于表示泛型。
  • <>中可以有多个标识符,也就是可以定义多个泛型类型。(如Java标准库的Map
泛型类

定义格式:

修饰符 class 类名<类型> { ... }

示例代码:

public class Generic {
    private T t;
    
    public Generic(T t) { this.t = t; }
    
    public T getT() { return t; }
    public void setT(T t) { this.t = t; }
}

注意:异常类不能是泛型的。

泛型方法

定义格式:

修饰符 <类型> 返回值类型 方法名(类型 变量名) { ... }

示例代码:

public  void show(T t) {
    System.out.println(t);
}

说明:

  • 泛型方法与所属的类是不是泛型类无关。
  • 泛型方法,可以声明为静态的。因为泛型方法的类型参数是在调用方法时确定的,而非在实例化类时确定。
静态方法

编写泛型类时,要特别注意,类的泛型类型不能用于静态方法。例如:

public class Generic {
    // 对静态方法使用:
    public static Generic create(T t) {
        return new Generic(t);
    }
}

上述代码会导致编译错误,我们无法在静态方法create()的方法参数和返回类型上使用泛型类型T。

原因是,类的泛型类型是在类创建实例时确定的,而类的静态方法是在类创建实例前就可以调用的,所以静态方法无法使用类的泛型类型

解决方式:

对于静态方法,我们可以单独改写为泛型方法。对于上面的create()静态方法:

public class Generic {
    public static  Generic create(E e) {
        return new Generic(e);
    }
}

当然,也可以写为,但是这样就可能与类的泛型类型弄混,降低代码的可读性。改为另一种泛型类型,才能清楚地将静态方法的泛型类型和实例类型的泛型类型区分开。

泛型接口

定义格式:

修饰符 interface 接口名<类型> { ... }
继承泛型类

父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:(实现泛型接口同理)

  • 子类不保留父类的泛型:按需实现

    • 没有类型:擦除,默认 Object 类型

      • public class SubGeneric extends Generic {
        }
        
    • 具体类型

      • public class SubGeneric extends Generic {
        }
        
  • 子类保留父类的泛型:泛型子类

    • 全部保留

      • public class SubMap extends Map {
        }
        
    • 部分保留

      • public class SubMap extends Map {
        }
        

同时,子类也可以有自己的泛型。

  • public class SubGeneric extends Generic {
    }
    
泛型的实现方式:擦拭法

泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。

泛型是一种类似”模板代码“的技术,不同语言的泛型实现方式不一定相同。

什么是擦拭法

Java语言的泛型实现方式是擦拭法(Type Erasure):

  • 编译器把类型转换成Object;
  • 编译器根据实现安全的强制转型。

虚拟机其实对泛型一无所知,所有的工作都是编译器做的。

例如,我们编写了一个泛型类Pair,这是编译器看到的代码:

public class Generic {
    private T t;
    
    public Generic(T t) { this.t = t; }
    
    public T getT() { return t; }
    public void setT(T t) { this.t = t; }
}

而虚拟机根本不知道泛型。这是虚拟机执行的代码:

public class Generic {
    private Object t;
    
    public Generic(Object t) { this.t = t; }
    
    public Object getT() { return t; }
    public void setT(Object t) { this.t = t; }
}

使用泛型的时候,我们编写的代码也是编译器看到的代码:

Generic p = new Generic<>("Hello");
String t = p.getT();

而虚拟机执行的代码并没有泛型:

Generic p = new Generic("Hello");
String t = (String) p.getT();

所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

擦拭法带来的局限

局限一:只能是类,而不能是基本类型。因为实际类型是Object,Object类型无法持有基本类型:

Pair p = new Pair<>(1, 2); // compile error!
泛型与继承多态:通配符

泛型在继承上的体现

向上转型

在Java标准库中的ArrayList实现了List接口,它可以向上转型为List

List list = new ArrayList();

但是,我们不能把ArrayList向上转型为ArrayList或List

因为,假设ArrayList可以向上转型为ArrayList,我们将可以把Float类型数据也传入ArrayList,这背离了引入泛型的初衷(类型安全)。因此,编译器为了避免这种错误,不允许把ArrayList转型为ArrayList

ArrayList和ArrayList两者完全没有继承关系

通配符

现在看来,我们只能通过多态机制限制集合容器类是某个类的子类,但是无法通过多态机制限制中的类型参数,这主要是由于类型安全问题。为了解决类型安全问题,Java 有了通配符。

通配符的分类:
  • 无限定通配符:
    • 可以匹配任何类的泛型。(无穷小 , 无穷大)
    • List这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素添加到其中
  • 上界通配符(Upper Bounds Wildcards):
    • 使用时传入的类型,必须继承指定类,或者实现指定接口。
    • 理解:把 ? 看作泛型本身,它 extends (继承)了指定类型
    • 例如: 代表继承了Number的泛型,即只匹配Number及其子类的泛型。
    • 从数学上可以理解为:(无穷小 , Number]
  • 下界通配符:
    • 使用时传入的类型不能小于指定类。
    • 理解:把 ? 看作泛型本身,它 super (大于)了指定类型
    • 例如: 代表大于Number的泛型,即只匹配Number及其父类的泛型。
    • 从数学上可以理解为:[Number , 无穷大)
通配符的作用: 1. 限定T类型

通过使用通配符,我们就可以实现泛型的向上转型,比如List这种带通配符的List表示它是各种泛型为继承了Number的类的List的父类。此时,我们就可以利用多态来实现对泛型类型的限定。

示例:

int sumOfList(List list) {
    int sum = 0;
    for (int i=0; i 

上述sumOfList()函数就保证了传入的List保存的一定是数字类型,从而可以进行读取数字并求和的操作。而不会出现传入了存了String的List从而无法求和的问题。

注:无限定通配符很少使用,因为它没有对泛型类型作任何限定,而且大多可以用替换。例如

static boolean isNull(Pair p) {
    return p.getFirst() == null || p.getLast() == null;
}

可以把上述代码中的无限定通配符用替换:

static  boolean isNull(Pair p) {
    return p.getFirst() == null || p.getLast() == null;
}
2. 限制读写操作

正如先前所说的,假设ArrayList可以向上转型为ArrayList,我们将可以把Float类型数据也传入ArrayList,这会导致类型安全问题。

同样,向上转型为ArrayList,逻辑上也存在类型安全问题。因此 Java 编译器对此做出了限制,也就是限制对进行写入操作,否则就会产生编译错误:

void test(List list) {
    // ...
    list.add(100); // compile error!
}

相应的,编译器也限制了对进行读取操作,限制了进行写入和读取操作。

不过,这种限制实际上是对赋值时的类型匹配进行限制,也就是说方法是可以运行的,但是参数类型不匹配,所以无法调用。

对于写入操作的限制:

  • list.add()方法是可以运行的,但是由于传入参数时,实参与形参的类型无法匹配,导致传入参数失败,所以无法成功调用方法。
  • 这很好理解,前面我们说过,可以理解为 ( 无穷小 , T ],我们假设List存储的类型是“无穷小”,此时你无论传入什么类型的参数都不可能继承于“无穷小”,也就不可能通过多态传入参数。

对于读取操作的限制也是同理:

  • list.get()方法,返回的值与接收的变量的类型无法匹配,所以无法赋值成功,但是方法依然可以调用,甚至可以直接传入System.out.println()函数进行打印。
  • 这很好理解,前面我们说过,可以理解为 [ T , 无穷大 ),我们假设List存储的类型是“无穷大”,此时你无论用什么类型的参数接收返回值,都不可能比“无穷大”的类型更大,也就不可能接收成功。

但是这种限制有一种弊端,那就是管不住*“无穷小”(null)“无穷大”(Object类)*本身。

  • 当我们调用“写入”方法时,我们可以传入“无穷小”(null),因为任何类型的变量都可以接收null,所以调用成功。
  • 当我们调用“读取”方法时,我们可以用“无穷大”(Object类)接收返回值,因为Object类的变量可以接收任何类型的数据,所以接收成功。

综上,我们可以用类型通配符限制读写操作:

  • 允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
  • 允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。

Java标准库的Collections类定义的copy()方法,就利用了这个功能:

public class Collections {
    // 把src的每个元素复制到dest中:
    public static  void copy(List dest, List src) {
        for (int i=0; i 

这个copy()方法的定义就完美地展示了extends和super的意图:

  • copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;
  • copy()方法内部也不会修改src,因为不能调用src.add(T)。

这个copy()方法的另一个好处是可以安全地把一个List添加到List,但是无法反过来添加:

// copy List to List ok:
List numList = ...;
List intList = ...;
Collections.copy(numList, intList);

// ERROR: cannot copy List to List:
Collections.copy(intList, numList);

而这些都是通过super和extends通配符,并由编译器强制检查来实现的。

父子原则:

何时使用extends,何时使用super?为了便于记忆,我个人使用父子原则来记忆:

  • 爸爸是,因为他比大;儿子是,因为他比小。
  • 爸爸可以给儿子写名字,但儿子不能写而只能读爸爸的名字。
  • 所以能写不能读,能读不能写。
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/294076.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号