[Java基础篇]对Java泛型的剖析

在这里插入图片描述

一、泛型详解

1.1 泛型的定义以及存在意义
☞ 泛型的定义:

泛型(Generic type或 generics),即参数化类型,是JavaSE 1.5新特性,对Java语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

☞ 为什么使用泛型?

为了解决数据在装入集合时的类型都被当做Object对待,从而失去本身特有的类型,从集合读取时,还要强制转换,java是所谓的静态类型语言,意思是在运行前,或者在编译期间,就能够确定一个对象的类型,这样做的好处是减少运行时由于类型不对引发的错误。但是强制类型转换是钻了一个空子,在编译期间不会有问题,而在运行期间,就有可能由于错误的强制类型转换,导致错误。这个编译器无法检查到。有了泛型,就可以用不着强制类型转换,在编译期间,编译器就能对类型进行检查,杜绝运行时由于强制类型转换导致的错误。

☞泛型的好处:

Java语言引入泛型是一个较大的功能增强。不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经称为泛型化的了。
1.类型安全。 泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
2.消除强制类型转换。 泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
3.潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。
简单地说,泛型简单易用、消除强制类型转换、保证类型安全。

1.2 泛型类、泛型方法、泛型接口的使用
1.2.1 泛型类的定义与使用

定义格式:修饰符 class 类名<代表泛型的变量> { }
如:class ArrayList < Iteger > { }

///在创建对象时确定泛型
ArrayList <String> list = new ArrayList<String>();
class ArrayList<String>{
  public boolean add(String e){  }
  
  public String get(int index){  }
  ....
  }

///自定义泛型
public class MyGenericsClass<MVP>{
   //没有MVP类型,在这里代表未知地一种数据类型未来传递什么就是什么类型
}

class ArrayList<E>{
  public boolean add(E e){  }
  
  public E get(int index){  }
  ....
  }
1.2.2 泛型方法的定义与使用

定义格式:修饰符 <代表泛型地变量> 返回值类型 方法名(参数) { }
如:public < MVP > void Method( ){…}

class MyGenericsMethod{
    public <MVP> void show(MVP mvp){
        System.out.println(mvp.getClass());///getClass()方法是获得调用该方法的对象的类
    }
}
public class Main{
    public static void main(String [] args){
      MyGenericsMethod my = new MyGenericsMethod();
      my.show("珠穆朗玛峰");
      my.show(8844.43);
    }
}

控制台结果输出:
在这里插入图片描述

1.2.3 泛型接口的定义与使用

定义格式:修饰符 interface 接口名 <代表泛型的变量> {…}
public interface MyGenericsInterrface < E >{…}

public interface MyGenericsInterface <E>{
   public abstract void add(E e);
   public abstract E getE();
}

///定义接口实现类时确定泛型的类型
public class Main implements MyGenericsInterface <String>{
    public void add (String e){
     ...
    }
    public String getE(){
      return null;
    }
}
///始终不确定泛型的类型
public class Main <E> implements MyGenericsInterface <E>{
    public void add (E e){
     ///省略...
    }
    public E getE(){
      return null;
    }
}
///创建对象时,确定泛型的类型
public class Main{
   public static  void main(String [] args){
        Main<String> my = new Main<String>();
        my.add("华山");
   }
}
1.3 泛型通配符类型和通配符的使用

当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过**<?>** 表示。但是一旦使用通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。

1.3.1 通配符基本使用(不受限泛型)

不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。此时只能接受数据,不能往集合中存储数据

import java.util.Collection;
import java.util.ArrayList;
public class Main{
    public static void main(String [] args){
      Collection<Integer> list1 = new ArrayList<Integer>();
      getElement(list1);

      Collection<String> list2 = new ArrayList<String>();
      getElement(list2);
    }
    public static void getElement(Collection <?> collection){
        ///?代表可以接收任意类型
    }
}
1.3.2 通配符高级使用(受限泛型)

设置泛型的时候,是可以任意设置的,只要是类就可以设置。但是在Java的泛型中可以指定一个泛型的上限和下限。

在引用传递中,泛型操作中也可以设置一个泛型对象的范围上限范围下限。范围上限使用extends关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类,而范围下限使用super关键字进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至Object类。

☞ 泛型的上限格式:类型名称 <? extends 类> 对象名称 (只能接收该类型及其子类)
☞ 泛型的下限格式:类型名称<? super 类> 对象名称(只能接收该类型及其父类型)

import java.util.Collection;
import java.util.ArrayList;
public class Main{
    public static void main(String [] args){
      Collection<Integer> list1 = new ArrayList<Integer>();///Integer类是Number类的子类
      Collection<String> list2 = new ArrayList<String>();
      Collection<Number> list3 = new ArrayList<Number>();
      Collection<Object> list4 = new ArrayList<>();///所有类的父类
      
        ///泛型上限的实例
      getElement1(list1);
      ///getElement1(list2);报错,不是Number类或其子类
      getElement1(list3);
      ///getElement1(list4);报错,不是Number类或其子类
      
      ///泛型下限的实例
      ///getElement2(list1);报错,不是Number类或其父类
      ///getElement2(list2);报错,Number类或其父类
      getElement2(list3);
      getElement2(list4);
    }
    public static void getElement1(Collection <? extends Number> coll){
        ///泛型上限:此时的泛型?,必须是Number类型或Number类的子类
    }

    public static void getElement2(Collection <? super Number> coll){
        ///泛型下限:此时的泛型?,必须是Number类型或Number类的父类
    }
}
1.4 泛型的实现方法:类型擦除

Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候去掉,这个过程称为类型擦除

import java.util.ArrayList;
public class Main{
    public static void main(String [] args) {
    ArrayList <String>  arrayList1 = new ArrayList<String>();
     arrayList1.add("华为");
     ArrayList <Integer>  arrayList2 = new ArrayList<Integer>();
        arrayList2.add(123);
        System.out.println(arrayList1.getClass()==arrayList2.getClass());///获取类的信息true
        ///说明String和Integer类型都擦除掉了,只剩下了原始类型
    }
}

无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删除类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定变量用Object)。
类型擦除后保留的原始类型:

class Pair<T>{
    private T first;
    private T second;
    public T getFirst(){
        return first;
    }
    public void setFirst(T first){
        this.first=first;
    }
}

///Pair<T>的原始类型
class Pair{
    private Object first;
    private Object second;
    public Object getFirst(){
        return first;
    }
    public void setFirst(Object first){
        this.first=first;
    }
    ...
}

如果类型变量有限定,那么原始类型就用第一个边界的类型变量来替换:

public class Pair <T extends Comparable&Serializable> {
}
///原始类型为Comparable
1.5 泛型中的约束及其局限性

这一小节将阐述Java泛型时需要考虑的一些限制。大多数限制都是由类型擦除引起的。

1.5.1 不能用基本类型实例化类型参数

不能用类型参数代替基本类型。因此,没有Pair< double >,只有Pair< Double >

1.5.2 运行时类型查询只适用于原始类型

虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
查询一个对象是否属于某个泛型类型时:

if(a instanceof Pair<String>)///编译错误
if(a instanceof Pair<T>)///编译错误

Pair<String> p = (Pair<String>)a;///warning,强制类型转换

Pair<String> stringPair = ...;
Pair<E> ePair = ...;
if(stringPair.getClass()==ePair.getClass())///编译通过,getClass()方法总是返回原始类型
1.5.3 不能创建参数化类型的数组
Pair<String>[] table = new Pair<String>[10];///编译错误

擦除之后,table的类型是Pair[ ],需要说明的是,只是不允许创建这些数组,而声明类型Pair< String >[ ]的变量仍是合法的,不过不能用new Pair< String >[10]初始化这个变量。
如果需要收集参数化类型对象,只有一种安全而有效的方法:使用
ArrayList:ArrayList<Pair< String >>

1.5.4 Varargs警告

向参数个数可变的方法传递一个泛型类型的实例。它的参数个数是可变的。

public static <T> void addAll(Collection<T> coll,T...ts){
    for(t: ts) ///枚举
        coll.add(t);///ts是一个数组,包含提供的所有实参。
}
///调用
Collection<pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table,pair1,pair2);

这种情况会得到一个警告。可以用以下两种方法抑制这个警告。
为包含addAll调用的方法增加注释 @Suppress Warns(“unchecked”)
在Java SE 7中,可以用 @SafeVarargs 直接标注addAll方法。

@SafeVarargs
public static <T> void addAll(Collection<T> coll,T...ts){
 ...
}
1.5.5 不能实例化类型变量

不能使用像 new T(…),new T[…]或T.class这样的表达式中的类型变量。下面的Pair构造器就是非法的

public Pair(){
    first = new T();
    second = new T();
}

类型擦除将T改变成Object,本意肯定不希望调用new Object()。在JavaSE8后,最好的解决办法是然调用者提供一个构造器表达方式:

Pair<String> p = Pair.makePair(String::new);

makePair方法接收一个Supplier,这是一个函数式接口,表示一个无参而且返回类型为T的函数:

public static <T> Pair<T> makePair(Supplier<T> const){
 return new Pair<>(const.get(),constr.get());
}
1.5.6 不能构造泛型数组

就像不能实例化一个泛型实例一样,也不能实例化数组。不过原因有所不同,毕竟数组会填充null值,构造时看上去是安全的。不过,数组本身也有类型,用来监控在虚拟机中的数组。这个类型会被擦除。

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

不能在静态域或方法中引用类型变量。

public class Pair<T>{
   private static T first;///编译错误
   public static T getFirst(){///编译错误
     if(first==null) return first;
   }
}
1.5.8 不能抛出或捕获泛型类的实例

既不能抛出也不能捕获泛型对象。实际上,甚至泛型扩展Throwable都是不合法的。

1.5.9 可以消除对受查异常的检查

Java异常处理的一个基本原则是,必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。通过使用泛型类、擦除和@Suppress Warning注解,就能消除Java类型系统的部分基本限制。

1.5.10 注意擦除后的冲突

当泛型类型被擦除时,无法创建引发冲突的条件。

1.6 泛型类型的继承规则

在使用泛型类时,需要了解一些有关继承和子类型的准则。

泛型类型的继承原则:要么同时擦除,要么子类大于等于父类类型

☞属性类型: 在父类中,随父类而定;在子类中,随子类而定。
☞方法重写: 随父类而定。
1.子类声明为具体类型(具体类型大于泛型):

public abstract class Father<T>{
   T name;
   public abstract void test(T name);
}
class Child extends Father<Integer>{
 public void test(Integer name){
  }
}

2. 子类声明为泛型(子类有两个,大于父类一个):

class Child<T,T1>  extends Father<T>{
  public void test(T name){///重写的方法随父类
  }
}

3. 子类为泛型,父类不指定,使用Object替代(子类一个,大于父类没有):

class Child<T> extends Father{
   public void test(Object name){
   }
}

4.子类与父类同时擦除,用Object替代:

class Child extends Father{
  public void test(Object name){
  }
}

有待更新…

二、参考资料

1.Java泛型详解
2.全网Java泛型最详解
3.猿问
4.Java中泛型的作用和意义
5.受限泛型
6.《Java核心技术 卷Ⅰ 基础知识》–泛型程序设计
7.类型擦除以及类型擦除带来的问题
8.3.泛型类型的继承规则

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中,获取lambda表达式的泛型类型是不直接支持的。Lambda表达式在编译时被转换为函数式接口的实例,而函数式接口可能会定义一个或多个泛型参数。 在Java中,我们不能直接获取lambda表达式的泛型类型,因为泛型类型被擦除了(type erasure)。在运行时,泛型类型信息是不可用的。Lambda表达式只能通过函数式接口中定义的方法来访问参数和返回类型。 然而,可以通过一些间接的方式来获取lambda表达式的泛型类型。例如,可以使用反射来获取函数式接口的类型信息,并进一步获取泛型参数的类型。以下是一个示例: ```java import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; public class Main { public static void main(String[] args) { MyInterface<String> myObj = s -> System.out.println(s); Type type = myObj.getClass().getGenericInterfaces()[0]; if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) type; Type[] typeArguments = parameterizedType.getActualTypeArguments(); for (Type typeArgument : typeArguments) { System.out.println(typeArgument); } } } } interface MyInterface<T> { void doSomething(T t); } ``` 在上述示例中,我们创建了一个使用String类型的lambda表达式,并使用反射来获取该lambda表达式实现的函数式接口的泛型类型。需要注意的是,这种方式只适用于已知函数式接口的情况,并且只能获取泛型参数的类型,无法获取具体的类型参数。 总结起来,虽然在Java中不能直接获取lambda表达式的泛型类型,但可以通过反射间接获取函数式接口的泛型信息。然而,这种方式的适用范围有限,并且可能会增加代码的复杂性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值