1、概述

泛型的概念:

泛型让类、接口或方法在定义时不指定具体类型,而在使用时再确定类型。

换句话说:泛型让代码在“写的时候”更通用,在“用的时候”更安全。

泛型的好处:类型安全、消除强制类型转换

2、泛型类

2.1 泛型类的定义

● 泛型类的定义语法:

class 类名称<泛型标识, 泛型标识, ...> {

private 泛型标识 变量名;

.....

}

● 常用的泛型标识:T、E、K、V...

泛型类定义举例:

package com.itheima.demo2;

/**

* 泛型类的定义

* @param 泛型标识——类型形参

* T 创建对象的时候指定具体的类型

*/

public class Generic {

private T key;

public Generic(T key) {

this.key = key;

}

public T getKey() {

return key;

}

public void setKey(T key) {

this.key = key;

}

}

2.2 泛型类的使用

● Java1.7以后,后面的<>中的具体的数据类型可以省略不写

类名<具体的数据类型> 对象名 = new 类名<>();

泛型类的使用举例:

Generic generic = new Generic<>("a");

2.3 泛型类的注意事项

①泛型类在创建对象的时候,没有指定类型,将按照Object类型来操作。

Generic generic = new Generic("ABC");

Object key3 = generic.getKey();

System.out.println("key3:" + key3);

②泛型类,不支持基本数据类型。

Generic generic1 = new Generic(100);

③泛型类虽然指定的类型是不同的,但本质上是同一个类型

System.out.println(intGeneric.getClass());

System.out.println(strGeneric.getClass());//他俩输出结果是一样的

2.4 从泛型类派生的子类

●子类也是泛型类的情况下,子类和父类的泛型类型要一致

class ChildGeneric extends Generic

●子类不是泛型类的情况下,父类要明确泛型的数据类型

class ChildGeneric extends Generic

举例说明 子类是泛型类:

//这是一个父类 是泛型类

public class Parent {

private E value;

public E getValue() {

return value;

}

public void setValue(E value) {

this.value = value;

}

}

//若此时定义了一个子类,要继承父类(标识为T),那它的泛型标识也一定是T,要不然报错

public class ChildFirst extends Parent {

@Override

public T getValue() {

return super.getValue();

}

}

举例说明子类不是泛型类:

//子类不是泛型类,父类是泛型类的话,不能写泛型类型,必须指定父类数据类型,否则报错

public class ChildSecond extends Parent {

@Override

public Integer getValue() {

return super.getValue();

}

@Override

public void setValue(Integer value)

3、泛型接口

3.1 泛型接口的定义

● 泛型接口的定义语法

interface 接口名称<泛型标识, 泛型标识, ...> {

泛型标识 方法名();

.....

}

3.2 泛型接口的使用

和前面的泛型类很类似

● 实现类不是泛型类的情况下,接口要明确数据类型

//这是一个泛型接口

public interface Generator {

T getKey();

}

//这不是一个泛型类,实现了泛型接口

public class Apple implements Generator {

@Override

public String getKey() {

return "hello generic";

}

}

● 实现类也是泛型类的情况下,实现类和接口的泛型类型要一致

//这是一个泛型接口

public interface Generator {

T getKey();

}

//这是一个泛型类,实现了泛型接口

public class Pair implements Generator {

@Override

public T getKey() {

return null;

}

}

4、泛型方法

泛型方法的定义:泛型方法,是在调用方法的时候指明泛型的具体类型

你可能会问,刚刚讲泛型类的时候不是已经包含了泛型方法吗?

那个其实是属于泛型类的成员方法,不是泛型方法!

泛型方法是我有泛型列表的,而泛型类的成员方法只在参数列表中有泛型标识

4.1 泛型方法

语法

修饰符 返回值类型 方法名(形参列表) {

方法体...

}

● public 与返回值中间的 非常重要,可以理解为声明此方法为泛型方法。只有声明了 的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。

举个例子

public class GenericMethodExample {

// 泛型方法:交换任意类型数组中两个元素的位置

public static void swap(T[] array, int i, int j) {

T temp = array[i];

array[i] = array[j];

array[j] = temp;

}

}

就是泛型列表,它告诉编译器:“我这个方法里有一个类型参数 T,可以在方法体中使用”。

后面的 T[] array 是使用这个类型参数。

我们也可以在泛型类中使用泛型方法,泛型方法的泛型标识独立于泛型类(即便是相同标识)

public class Box {

private T value;

public Box(T value) {

this.value = value;

}

// 泛型方法(与类的泛型参数 T 无关)

public void print(E element) {

System.out.println("打印内容:" + element);

}

}

这个方法在 void 前面又声明了一个新的类型参数 ;它与类的泛型参数 T 没有任何关系;

请注意,泛型类中的方法不能是 static,如果它要用类的泛型参数

但是泛型方法可以是 static的(泛型方法的类型参数是方法级别的,不依赖于类的泛型参数。)

4.2 泛型方法与可变参数

语法

public void print(E... e) {

for (E el : e) {

System.out.println(e);

}

}

举个例子

public static void print(E... e) {

for (int i = 0; i < e.length; i++) {

System.out.println(e[i]);

}

}

Box intBox = new Box<>();

Box strBox = new Box<>();

4.3 泛型方法的总结

泛型方法能使方法独立于类而产生变化

如果 static 方法要使用泛型能力,就必须使其成为泛型方法

5、类型通配符

类型通配符一般是使用“?”代替具体的类型实参。

所以,类型通配符是类型实参,而不是类型形参。

我来细细讲讲,这里很绕

这个T,是形参,是在定义过程中写的

class Box { // T 是类型形参

private T value;

public T getFirst() { return value; }

public void setFirst(T value) { this.value = value; }

}

当别人使用时,会传入“实参类型” 这里的 Integer、String 就是 类型实参,代替了 T。

Box intBox = new Box<>();

Box strBox = new Box<>();

这个< ?>是实参,实在使用过程中写的

public static void showBox(Box box)

这个< ?>仿佛在说“我接收一个 Box,但是我不知道它装的是什么类型”,? 是“实参中的未知者”。

5.1 类型通配符的引出

①我们定义了一个泛型类 叫Box

public class Box {

private E first;

public E getFirst() {

return first;

}

public void setFirst(E first) {

this.first = first;

}

}

②又定义了一个静态方法,用于展示box对象的属性

Number是数值类型的共同父类(抽象类)。

public static void showBox(Box box) {

Number first = box.getFirst();

System.out.println(first);

}

在这里,请注意!!

Box它可以装任何 Number 及其子类的对象(比如 Integer、Double)。

所以:我写如下代码没问题

Box box1 = new Box<>();

box1.setFirst(100);

showBox(box1);

但是写如下代码会出问题

Box box2 = new Box<>();

box2.setFirst(200);

showBox(box2);//这里会报错

为什么会报错?因为你往showBox中传递的参数是 Box 类型,但是showBox所需要的是Box类型!

那怎么办?用类型通配符

public static void showBox(Box box) {

Object first = box.getFirst();

System.out.println(first);

}

5.2 类型通配符的上限

定义:

类/接口

根据我们刚刚的例子,可以传递Box 也可以是Box类型,还可以是其他类型

但我们设置了类型通配符的上限后(比如上限是Number):

public static void showBox(Box box) {

Number first = box.getFirst();

System.out.println(first);

}

代表着,我只可以接收Box的泛型是Number或其子类的对象

注意一点,我们不能在box中add元素,我举例说明:

我们有一个简单的继承结构:

class Animal {}

class Cat extends Animal {}

class MiniCat extends Cat {}

然后我们准备三种容器:

List animals = new ArrayList<>();

List cats = new ArrayList<>();

List miniCats = new ArrayList<>();

定义一个方法,能接收装各种“猫或猫的子类”的容器:

public static void readCats(List list) {

Cat c = list.get(0); // ✅ 可以安全读取为 Cat

// list.add(new Cat()); // ❌ 不行

// list.add(new MiniCat()); // ❌ 不行

System.out.println(c.getClass().getSimpleName());

}

为什么 add 不行?

假设你是这样调用方法的:readCats(miniCats);

那么 list 实际指向 List。list中的元素必须是MiniCat类型和其子类,

如果我写了add(new Cat()),那肯定不对,因为Cat不是MiniCat类型和其子类

所以编译器为了安全,干脆禁止任何 add(除了 null)。

但你可以 get,因为无论实际是哪种猫,它都至少是个 Cat。

5.3 类型通配符的下限(1)

定义

类/接口

注意一点,我们不能在box中get元素,但是add可以:

和上面一样,我们有一个简单的继承结构:

class Animal {}

class Cat extends Animal {}

class MiniCat extends Cat {}

然后我们准备三种容器:

List animals = new ArrayList<>();

List cats = new ArrayList<>();

List miniCats = new ArrayList<>();

定义一个方法,能接收装各种“猫或猫的父类”的容器:

public static void addCats(List list) {

list.add(new Cat()); // ✅ 可以

list.add(new MiniCat()); // ✅ 也可以

// Cat c = list.get(0); // ❌ 不行,只能当 Object

Object obj = list.get(0); // ✅ 只能读成 Object

}

为什么 add 可以?

假设传入的是:addCats(animals);

list 实际是 List ,所以list中必须是Animal或其子类,往 List 里加 Cat 或 MiniCat,当然安全。

但如果反过来你想 Cat c = list.get(0),编译器拒绝:

——因为它不知道你传进来的是不是 List,如果是Object,你为什么要用Cat接收?

安全起见,只能当作 Object 读取。

5.4 类型通配符的下限(2)

这里讲了应用 以TreeSet为例,其中有两个构造方法

我们讲第一个,传构造器的

我们创建了三个类Animal、Cat、MiniCat

// 父类 Animal

public class Animal {

public String name;

public Animal(String name) {

this.name = name;

}

@Override

public String toString() {

return "Animal{" +

"name='" + name + '\'' +

'}';

}

}

// 子类 Cat,继承自 Animal

public class Cat extends Animal {

public int age;

public Cat(String name, int age) {

super(name);

this.age = age;

}

@Override

public String toString() {

return "Cat{" +

"age=" + age +

", name='" + name + '\'' +

'}';

}

}

// 子类 MiniCat,继承自 Cat

public class MiniCat extends Cat {

public int level;

public MiniCat(String name, int age, int level) {

super(name, age);

this.level = level;

}

@Override

public String toString() {

return "MiniCat{" +

"level=" + level +

", age=" + age +

", name='" + name + '\'' +

'}';

}

}

又定义了三个比较器,用于TreeSet(一个类对应一个)

class Comparator1 implements Comparator {

@Override

public int compare(Animal o1, Animal o2) {

return o1.name.compareTo(o2.name);

}

}

class Comparator2 implements Comparator {

@Override

public int compare(Cat o1, Cat o2) {

return o1.age - o2.age;

}

}

class Comparator3 implements Comparator {

@Override

public int compare(MiniCat o1, MiniCat o2) {

return o1.level - o2.level;

}

}

然后我们应用

public class Test08 {

public static void main(String[] args) {

TreeSet treeSet = new TreeSet<>(new Comparator2());

treeSet.add(new Cat("jerry", 18));

treeSet.add(new Cat("amy", 22));

treeSet.add(new Cat("frank", 25));

treeSet.add(new Cat("jim", 15));

for (Cat cat : treeSet) {

System.out.println(cat);

}

}

}

发现结果可以按照age排序

如果传入父类Animal的比较器呢?

发现也是可以的

如果传入子类minicat的比较器呢

是不可以的!报错!因为只可以传Cat及其父类。从直觉上来说也容易理解,minicat多了一个属性,TreeSet中又没有这个属性,没法比较

6、类型擦除

概念:

泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的,但是,泛型代码能够很好地和之前版本的代码兼容。那是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除。我们称之为——类型擦除。

举个例子:

public static void main(String[] args) {

ArrayList intList = new ArrayList<>();

ArrayList strList = new ArrayList<>();

System.out.println(intList.getClass().getSimpleName());

System.out.println(strList.getClass().getSimpleName());

}

运行后发现

他们的类都是一样的,所以这也就解释了,为什么我们编写代码的时候,看似类型不一样,一个是ArrayList、另外一个是 ArrayList,但是运行后其实是归属于一个类型,因为Java 的泛型在编译后被擦除

6.1 无限制类型擦除

什么意思呢?当我的泛型标识为T的时候,在编译后,T的类型会被擦除,变成Object

举例说明:

我定义了一个泛型类

public class Erasure {

private T key;

public T getKey() {

return key;

}

public void setKey(T key) {

this.key = key;

}

}

写了一个方法,输出泛型类的属性和类型

import java.lang.reflect.Field;

public class TestErasure {

public static void main(String[] args) {

// 创建泛型对象

Erasure erasure = new Erasure<>();

erasure.setKey(123);

// 利用反射,获取 Erasure 类的字节码文件的 Class 对象

Class clz = erasure.getClass();

// 获取所有的成员变量

Field[] declaredFields = clz.getDeclaredFields();

// 打印成员变量的名称和类型

for (Field declaredField : declaredFields) {

System.out.println(declaredField.getName() + ":" + declaredField.getType().getSimpleName());

}

}

}

输出:

所以,如果泛型类的泛型列表只有一个T,那类型擦除之后,确实是类型转换成了Object

6.2 有限制类型擦除

什么意思呢?当我的泛型标识为T entends Number的时候,在编译后,T的类型会被擦除,变成Number

举个例子

我定义了一个泛型类

public class Erasure {

private T key;

public T getKey() {

return key;

}

public void setKey(T key) {

this.key = key;

}

}

写了一个方法,输出泛型类的属性和类型

import java.lang.reflect.Field;

public class TestErasure {

public static void main(String[] args) {

// 创建泛型对象

Erasure erasure = new Erasure<>();

erasure.setKey(123);

// 利用反射,获取 Erasure 类的字节码文件的 Class 对象

Class clz = erasure.getClass();

// 获取所有的成员变量

Field[] declaredFields = clz.getDeclaredFields();

// 打印成员变量的名称和类型

for (Field declaredField : declaredFields) {

System.out.println(declaredField.getName() + ":" + declaredField.getType().getSimpleName());

}

}

}

输出为:

所以,如果泛型类的泛型列表是T extends Number,那类型擦除之后,确实是类型转换成了Number

上面我说是泛型类,泛型方法也一样!!

6.3 桥接方法

桥接方法存在于泛型接口中

可以看见,泛型标识被擦除成了Object,实现类没有泛型标识,所以不变。除此之外,还产生了一个桥接方法,用于保持接口和类的实现关系

为什么会有这个桥接方法?

擦除后,InfoImpl 这个类的 info(Integer) 方法不再匹配 接口里的 info(Object) 方法签名。

也就是说:

接口现在声明的是 Object info(Object var),

而实现类提供的是 Integer info(Integer var)——

两者在 JVM 层面是不同的签名,

如果编译器不处理,多态调用会失效。

举个例子

定义了一个接口

public interface Info {

T info(T t);

}

定义了接口的实现类

public class InfoImpl implements Info {

@Override

public Integer info(Integer value) {

return value;

}

}

用反射,写一个方法,获取实现类的方法名、方法类型

Class infoClass = InfoImpl.class;

// 获取所有的方法

Method[] infoImplMethods = infoClass.getDeclaredMethods();

for (Method method : infoImplMethods) {

// 打印方法名称和方法的返回值类型

System.out.println(method.getName() + ":" + method.getReturnType().getSimpleName());

}

运行结果如下

第二个info就是生成的桥接方法

7、泛型与数组

7.1 数组的协变

讲正式内容之前,先讲一下数组的协变

想象你有个动物园系统:

Cat 是 Animal 的子类。

一个 Animal[]数组 可以装各种动物。

那你说:能不能用一个 Cat[] 去当作 Animal[] 使用?

Java 说:可以!

Cat[] cats = new Cat[3];//这是新创建了一个数组,数组引用是放在cats变量里,指向的是实际的地址Cat[3]

Animal[] animals = cats; // ✅ 允许 将刚才的地址给animals,现在,animals和cats都存放着同样的引用,指向实际的地址Cat[3]

这就是 协变 —— Cat 是 Animal 的子类型,所以 Cat[] 也被当作 Animal[] 的子类型。

所以我可以往数组里new一个cat对象

animals[0] = new Cat(); // cats[0] 也跟着变

由于数组是协变的,我也可以放其他Animal子类对象,于是我放一个dog对象

animals[1] = new Dog();

在编译时期确实没问题,编译器会说:“没毛病啊,Dog 是 Animal 嘛。”

但是在运行的时候出问题了,JVM 在运行时检查到这块堆内存的真实类型是 Cat[],

于是报错!

ArrayStoreException: java.lang.Dog

JVM 拒绝往 Cat 数组里塞一只狗。

所以,数组是协变的,这本身就是不合理的,所以在开发中,即使是能用,也尽量少用或不用

7.2 泛型数组的创建

可以声明带泛型的数组引用,但是不能直接创建带泛型的数组对象

举例:

ArrayList[] listArr = new ArrayList[5];//这样是错误的

ArrayList[] arr = new ArrayList[5];//这样是正确的

可以通过 java.lang.reflect.Array 的 newInstance(Class, int) 方法创建 T[] 数组

举例:

我创建了一个操作数组的对象

public class Fruit {

private T[] array;

public Fruit(Class clz, int length) {

// 通过 Array.newInstance 创建泛型数组

array = (T[]) Array.newInstance(clz, length);

}

/**

* 填充数组

* @param index

* @param item

*/

public void put(int index, T item) {

array[index] = item;

}

/**

* 获取数组元素

* @param index

* @return

*/

public T get(int index) {

return array[index];

}

public T[] getArray() {

return array;

}

}

就可以操作它

Fruit fruit = new Fruit<>(String.class, 3);

fruit.put(0, "苹果");

fruit.put(1, "西瓜");

fruit.put(2, "香蕉");

System.out.println(Arrays.toString(fruit.getArray()));

输出:

7.3 泛型数组的陷阱(这里没看懂没关系)

先看一个经典的泛型数组陷阱

public static void main(String[] args) {

ArrayList[] list = new ArrayList[5];

ArrayList[] listArr = list;

ArrayList intList = new ArrayList<>();

intList.add(100);

list[0] = intList; // ⚠️ 这里的赋值虽然能编译,但留下了隐患

String s = listArr[0].get(0); // 💥 运行时出错

System.out.println(s);

}

我讲一下代码执行的流程:

①ArrayList[] list = new ArrayList[5];

声明一个变量 list,它指向 ArrayList 数组 类型的对象

在堆里创建了一个能装 5 个 ArrayList 对象的数组。

编译器和 JVM 都知道:这个数组的类型是“ArrayList 的数组”。

它可以装任何种类的 ArrayList(比如装 ArrayList、ArrayList 等)。

②ArrayList[] listArr = list;

编译器角度:

我要声明一个变量 listArr,它的类型是 ArrayList[],也就是说,这个变量在语义上只能指向装 ArrayList 的数组。

但右边其实给我的 list 是个 ArrayList[](原始类型数组),

规范允许:如果你使用了 raw type(比如 ArrayList),编译器允许把它赋给一个带泛型参数的类型,虽然不安全,我就让它过吧,但我得给个 unchecked warning。”

也就是说

list 的静态类型是 ArrayList[],所以它认为:

“list 是一个能装 ArrayList 对象的数组。”

listArr 的静态类型是 ArrayList[],所以它认为:

“listArr 是一个能装 ArrayList 对象的数组。”

运行时角度(JVM):

“行,我就让两个变量 listArr 和 list 指向同一块数组内存。”

于是,list和listArr都指向同一个地址

只是编译器以为” listArr 里面的每个元素都是 ArrayList

而运行起来的 JVM 知道:“其实就是普通的 ArrayList 啊。”

③ArrayList intList = new ArrayList<>();

intList.add(100);

list[0] = intList;

讲一下流程:

在栈内创建一个变量(引用变量),名字叫 intList;在堆内创建一个新的 ArrayList 对象,类型是 ArrayList,然后让 intList 指向这个对象。

然后往 intList 所指向的 ArrayList 对象中添加一个元素 100。所以现在堆内的那个 ArrayList 对象状态是 [100]

数组 list 的元素类型是 ArrayList,每个格子(list[0]、list[1] …)存放的不是对象本身,而是对象的引用(地址)。

④String s = listArr[0].get(0);出问题了!

编译时:

编译器看到 listArr 是 ArrayList[]。

它想:“你取出来的肯定是个 String,我就让你赋给 String s 吧。”

运行时:

JVM 去 listArr[0] 看,发现那是 ArrayList

它取出了 Integer 100,尝试给 String s。

就抛出了异常!ClassCastException。

Copyright © 2088 世界杯亚洲预选赛赛程_世界杯的 - qxcnz.com All Rights Reserved.
友情链接