一、为什么要使用泛型
总结就是: 可读性,类型安全 ,代码复用
/*坐标点*/
public class Point <T>{
private T x; //坐标x
private T y; //坐标y
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public T getY() {
return y;
}
public void setY(T y) {
this.y = y;
}
}
1、使代码具有更好的可读性
- 代码具有更好的可读性,人们一看就知道集合中元素的类型;
- 使不同的类型数据能够重用相同的代码,使代码的抽象等级变的更高;Point可以传入多中类型的数据,如(10,20)、(10.5,20.5)、(“东经115.7°”,“,北纬40.6°”)。
- 使代码书写变得更加优雅,减少了很多样板方法,JAVA没有引入泛型前,代码使用Object接收所有类型,使用具体类型前需要进行性类型转换,需参数类型进行检查;
2、代码类型转换变的更加安全
- 解决了数据类型的安全性问题,限制Point中x,y的数据类型保持一致,若使用Object接收则需要增加更多的校验限制;
- 避免了代码层面的类型强转的问题,避免了ClassCastException的出现;
// Point中 x,y的类型是Object
Point p = ...;
p.setX("东经115.7°");
int x = (Integer)p.getX();//这里就会出现ClassCastException的出现
- 然数据类型的安全问题在编译器就能够被发现。若没有类型错误检查,存在向集合中添加不同类型的数据问题,若没用引入泛型,像ArrayList这样的集合类里面,需要维护一个Obejct引用的数组,在获取值时必须进行类型的强制转换,此外没有类型错误检查,就可以向Object数组中添加任何的值。
二、泛型类的定义
(一)类型参数(type parameter)
var files = new ArrayList();
Java 10中引入var关键字,它可以让我们在声明变量时省略类型信息,有编译器根据上下文进行类型推断。(这种类型推断的功能可以简化代码,提高代码的可读性和编写效率)
Arraylist files = new Arraylist<>();
如果用一个明确的类型而不是var声明一个变量,“菱形”操作符中的类型可以省略。
ArrayList<String> passwords = new ArrayList<String>(){
@Override
public String get(int index) {
return super.get(index).replaceAll(".","*");
}
};
Java 9扩展了菱形语法的使用范围,原先不接受这种语法的地方现在也可以使用了, 去除了后面“菱形”操作符中的类型参数
ArrayList<String> passwords = new ArrayList<>(){
@Override
public String get(int index) {
return super.get(index).replaceAll(".","*");
}
};
(二)泛型类(generic class)
public class ClassName<T,R> {...}
在不同的书中,T,R有被称为<泛型标识>,也有的称为<类型参数>,还有…;
<泛型标识1,…,泛型标识n>
多个类型参数使用“,”分割;
声明泛型类的基本语法如上,在类名的后面加上菱形运算符;
类型参数T,R在类定义中,可以用做方法的返回值,方法的入参,局部变量、成员变量的类型。
常用字母含义
- E 集合
- K 键 key
- V 值value
- R 返回值 return value
- T 类型 type
泛型类相当于普通类的工厂。
(三)泛型方法
/*下面四个方法都是正确的*/
/*getMiddle1()与getMiddle2()是个比较*/
//getMiddle1() 若将<T>去掉会编译报错
public static <T> T getMiddle1(T... a) {
return a[a.length / 2];
}
public T getMiddle2(T... a) {
return a[a.length / 2];
}
public static <T> void getMiddle3(T... a) {
}
public static <T> int getMiddle4(T... a) {
return 0;
}
1、静态泛型方法一定要在修饰符之后、返回值之前增加<类型参数>,不然代码无法编译通过
原因:
Java的泛型方法属于‘伪泛型’,在代码编译的时候会进行类型擦除。普通的泛型方法在类构建时已经明确了泛型的<类型参数>。静态方法是随着类的加载而加载的,在加载类时,程序就会为静态方法分配内存。静态方法的加载先于类的实例化,而在静态泛型方法中,泛型的<类型参数>是无法直接推测的,不知道明确的类型。为什么不能省略是给静态泛型方法做类型推断的。
这个类型参数,可以在泛型方法调用时传入
String middle = ArrayAlg.<String>getMiddle("lefe","middle","right");
实际上大多情况下,方法调用中可以省略类型参数,编译器有足够的信息推断出你想要的方法
//id:1
System.out.println(ArrayAlg.getMiddle(3.12d, 50, 100).getClass().getName());//java.lang.Integer
//id:2
System.out.println(ArrayAlg.getMiddle( 50, 3.12d,100).getClass().getName());//java.lang.Double
//id:3
double middle1 =ArrayAlg.getMiddle(3.12d, 50, 100); //ERROR
//id:4
double middle2 =ArrayAlg.<Double>getMiddle(3.12d, 50, 100); //ERROR
简单地说, 编译器将把参数自动装箱为 1 个Double 和 2 个 Integer 对象, 然后寻找这些类的共同超类型。 事实上, 它找到了 2 个超类型:Number 和 Comparable 接口, Comparable 接口本身也是一个泛型类型。
使用id为1和2处的代码能通过,id为4处限定了<泛型类型>是Double所以两个Interger的地方报错了。
ArrayAlg.getMiddle(3.12d, 50, 100)得出的结果是Number&Comparable对象,无法转成Double对象,股整段报错。
double middle3 =(Double)ArrayAlg.getMiddle(3.12d, 50, 100);
这样编译可以通过,但会运行过后会出现问题
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Double
at com.wusp.generic.ArrayAlg.main(ArrayAlg.java:56)
小窍门
如果想知道编译器对一个泛型方法调用最终推断出哪种类型,若存在返回值,故意将这个泛型方法负责给一个错误的类型,我们可以通过Idea工具去看到泛型方法的最终推断出的类型。
三、类型擦除
Java的泛型方法属于‘伪泛型’,虚拟机中没有类型参数,代码结果编译期编译过后,类型变量会被擦除(erased),编译期会自动提供一个相应的原始类型(raw type) 去替换掉<类型参数>,对于无限定的变量则替换为Object。
类型变量的限定(重点看后面的通配符)
public static <T extends Comparable> T min(T[] a){...}
基本语法:
T extends Comparable & Serializable
表示T应该是限定类型(bounding type)的子类型(subtype)。T和限定类型可以是类,也可以是接口。选择关键字extends的原因是它更接近子类型的概念, 并且Java的设计者也不打算在语言中再添加一个新的关键字(如sub)。
一个类型变量或通配符可以有多个限定,T extends Comparable & Serializable
限定类型用 “ &“ 分隔, 而逗号用来分隔类型变量。
Pair中的类型变量没有显式的限定,因此,原始类型用Object替换T。泛型类型被擦除后,在class文件中获取T时,是存在类型强转的。
<T extends Comparable & Serializable> 原始类型用Comparable替换T。
<T extends Serializable & Comparable> 原始类型用Serializable替换T。编译器在必要时要向 Comparable 插入强制类型转换。 为了提高效率, 应该将标签 (tagging) 接口(即没有方法的接口)放在限定列表的末尾。
Pair<Emp loyee> buddi.es =... ;
Employee buddy = buddies. getFirst () ;
Pair擦除后的原始类型是Object,getFirst擦除类型后的返回类型是Object。编译器自动插入转换到Employee的强制类型转换。也就是说, 编译器把这个方法调用转换为两条虚拟机指令:
-
· 对原始方法Pair. getF扛st的调用 。
-
· 将返回的Object类型强制转换为Employee类型 。
public static T min(T[] a)
public static Comparable min(Comparable[] a)
桥方法:(等待补充)
Java5之前的代码兼容性问题
四、通配符类型(wildcard type)
1、extends
public class Pair<T> {
private T first;
private T second;
public Pair() {
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
public static void printBuddies(Pair<Employee> p){
Employee first= p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
}
Pair是无法传给printBuddies方法的。为了解决这一个限制,就引入了extends这个统配符。
public static void printBuddies3(Pair<? extends Employee> p){
Employee first= p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
}
限定Pair类型参数是类Employee或其子类。
使用通配符会通过Pirr<? extends Employee>的引用破坏Pa订吗?
public void setFirst(? extends Employee first)
public ? extends Employee getFirst()
这样将不可能调用setFirst方法。编译器只知道需要Employee的某个子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型。毕竟?不能匹配。
使用getFirst 就不存在这个问题:将getFirst的返回值赋给一个Employee引用是完全合 法的。
super
? super Manager
super通配符是限制<类型参数>为Manager的所有超类型。
super关键字与extends刚好相反,super决定了类的下限,extends决定了类的上限。
void setFirst(? super Manager)
? super Manager getFirst()
可以调用setFirst方法,因为传入的值都是Manager的父类,
只能使用Object去接收getFirst()
- <T extends Comparable<? super T>>
处理一个LocalDate对象的数组时,我们会遇到一个问题。LocalDate实现了ChronoLocalDate,而ChronolocalDate扩展了Comparable<ChronolocalDate>。因此,LocalDate实现的是Comparable<ChronolocalDate|>而不是Comparable<LocalDate>。
在这种情况下, 可以利用超类型来解决,它可以声明为使用类型 T的对象 ,或者也可以是使用T的一个超类型的对象(当T是 LocalDate时)
public static <T extends Comparable<? super T» T min(T[] a){...}
//现在compareTo方法写成
int compareTo(? super T)
- super另一个常见的用法是作为一个函数式接口的参数类型
Collection接口有一个方法:
default boolean removeIf(Predicate<? super E> filter)
这个方法会删除所有满足给定谓词条件的元素。 例如, 如果你不喜欢有奇怪散列码的员工,就可以如下将他们删除:
Arraylist<Employee> staff =... ;
Predicate<Object> oddHashCode = obj -> obj.hashCode() % 2 != 0;
staff.removeIf(oddHashCode);
你希望能够传入一个Predicate<0bject>, 而不只是Predicate<Employee>。super通配符可以使这个愿望成真。
?(无限定通配符)
Pair<?> 初看起来, 这好像与原始的Pair类型 一样。
? getFirst()
void setFirst(?)
getFirst()的返回值只能赋给一个Object ,
setFirst()方法不能被调用,甚至不能用Object 调用。可以调用setFirst(null)。
Pair<?>和原始Pair本质的不同在于:可以用任意Object 对象调用原始Pair类的setFirst 方法。
为什么要使用这样一个脆弱的类型?
它对于很多简单操作非常有用。例如, 下面这个方法可用来测试一个对组是否包含一个null引用,它不需要实际的类型。
public static boolean hasNulls(Pair<?> pair){
return pair.getFirst()==null || pair.getSecond()==null;
}
通过将hasNulls转换成泛型方法, 可以避免使用通配符类型:
public static boolean hasNulls(Pair p)
但是, 带有通配符的版本可读性更好。
五、泛型的限制与局限性
大多数限制都是由类型擦除引起的
1、不能用基本类型实例化类型参数
8中基本类型:boolean、char(字符型,2字节,18bit)、byte(8)、short(16)、int(32)、long(64)、float、double
不能用基本类型代替类型参数。因此,没有Pa订,只有Pa订。当然,其原因就在于类型擦除。擦除之后,Pair类含有Object类型的字段,而Object不能存储double值。这的确令人烦恼。但是,这样做与Java语言中基本类型的独立状态相一致。这并不是一 个致命的缺陷一只有8种基本类型,而且即使不能接受包装器类型(wrappertype),也可 以使用单独的类和方法来处理。
2、运行时类型查询只适用千原始类型
同样的道理,getClass方法总是返回原始类型。例如:
Pa订stringPair =…;
Pa订employeef’air =…;
if (stringPair.getClass() == employeePair.getClass()) // they are equal
其比较的结果是true,这是因为两次getClass调用都返回Pair.class。
if (a instanceof Pair) // ERROR
实际上仅仅测试a是否是任意类型的一个Pa订。下面的测试同样如此:
if (a instanceof Pair< T:,) / / ERROR 或强制类型转换:
Pair p = (Pair) a; // warning–can only test that a is a Pair
为提醒这一风险,如果试图查询一个对象是否属于某个泛型类型,你会得到个编译器错误(使用instanceof时),或者得到一个警告(使用强制类型转换时)。
3、不能创建参数化类型的数组
这有什么问题呢?擦除之后,table的类型是Pa订[]。 可以把它转换为Object [I: Object[] obja「ray table;
数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStoreException异常:
Java不支持泛型类型的数组。