Java泛型基础知识

一、为什么要使用泛型

总结就是: 可读性,类型安全 ,代码复用

/*坐标点*/
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
在这里插入图片描述
简单地说, 编译器将把参数自动装箱为 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;
数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStore­Exception异常:

Java不支持泛型类型的数组。

4、Varargs 警告

5、不能实例化类型变量

6、不能构造泛型数组

7、泛型类的静态上下文中类型变量无效

8、不能抛出或捕获泛型类的实例

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值