Java基础之泛型与通配符

Java基础之泛型与通配符

    • 泛型
      • 泛型三种使用方式
        • 1.泛型类:
        • 2.泛型接口
        • 3.泛型方法
      • 项目中用到的泛型
      • 泛型擦除机制(重点)
        • 桥方法
      • 泛型有哪些限制
      • 看看一下代码能否运行
    • 通配符
      • 通配符和常用的泛型E之间的区别
      • 无界通配符
        • List< ? >和List的区别
      • 上边界通配符,和下边界通配符

泛型

Java泛型(Generics) 是JDK5引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。

编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如ArraylList < Student >
students = new ArrayList < Student >() 这行代码就指明了该ArrayList对象只能传入Persion对象,如果传入其他类型的对象就会报错。

ArrayList<E> extends AbstractList<E>

并且,原生List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型之后编译器自动转换。

泛型三种使用方式

三种使用方式: 泛型类、泛型接口、泛型方法。

1.泛型类:

//此处E可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<E>{

    private E key;

    public Generic(E key) {
        this.key = key;
    }

    public E getKey(){
        return key;
    }
}

2.泛型接口

public interface Generator<E> {
    public E method();
}

实现泛型接口,不指定类型:

class GeneratorImpl<E> implements Generator<E>{
    @Override
    public E method() {
        return null;
    }
}

实现泛型接口,指定类型:

class GeneratorImpl<E> implements Generator<String>{
    @Override
    public String method() {
        return "hello";
    }
}

3.泛型方法

   public static < E > void printArray( E[] inputArray )
   {
         for ( E element : inputArray ){
            System.out.printf( "%s ", element );
         }
         System.out.println();
    }

使用:

// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray  );
printArray( stringArray  );

项目中用到的泛型

  • 自定义接口通用返回结果 CommonResult< E > 通过参数 E 可根据具体的返回类型动态指定结果的数据类型
  • 定义 Excel 处理类 ExcelUtil< E > 用于动态指定 Excel 导出的数据类型
  • 构建集合工具类(参考 Collections 中的 * Sort,binarySearch* 方法)。

泛型擦除机制(重点)

Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除。

编译器会在编译期间会动态地将泛型 E 擦除为 Object 或将 E extends xxx 擦除为其限定类型 xxx

因为,泛型本质上其实还是编译器的行为,为了保证引入泛型机制但不创建新的类型,减少虚拟机的运行开销,编译器通过擦除将泛型类转化为一般类。

举例说明:

List<Integer> list = new ArrayList<>();

list.add(12);
//1.编译期间直接添加会报错
list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//2.运行期间通过反射添加,是可以的
add.invoke(list, "kl");

System.out.println(list)

由于泛型擦除的问题,下面的方法重载会报错。

public void print(List<String> list)  { }
public void print(List<Integer> list) { }

泛型擦除之后, List< String >List< Integer > 在编译以后都编程 List

即然编译器要把泛型擦除,为什么还要用泛型?不用Object代替。
这个问题其实在变相考察泛型的作用:

  • 使用泛型可在编译期间进行类型检测。
  • 使用Object类型需要手动添加强制类型转换,降低代码可读性,提高出错率。
  • 泛型可以使用限定类型入 E extends Comparable

桥方法

桥方法(Bridge Method)用于继承泛型类时保证多态。

class Node<T> {
    public T data;
    public Node(T data) { this.data = data; }
    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }

  	// Node 泛型擦除后为 setData(Object data),而子类 MyNode 中并没有重写该方法,所以编译器会加入该桥方法保证多态
   	public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

注意: 桥方法为编译器自动生成,非手写。

泛型有哪些限制

泛型的限制一般有泛型擦除机制导致的。擦除为Object 后无法进行类型判断。

  • 只能声明不能实例化E 类型变量。
  • 泛型参数不能是基本类型。因为基本类型不是Object的子类,应用基本类型的包装类型代替。
  • 不能实例化泛型参数为数组。擦除后为Object 后无法进行类型判断。
  • 不能实例化泛型数组。
  • 泛型无法使用Instance ofgetClass() 进行类型判断。
  • 不能实现两个不同参数的同一接口,擦除后多个父类的桥方法将冲突
  • 不能使用 static 修饰泛型变量

看看一下代码能否运行

public final class Algorithm {
    public static <E> E max(E x, E y) {
        return x > y ? x : y;
    }
}

无法运行,因为x和y都会被擦除为 Object 类,Object 类无法使用 > 进行比较。

public class Singleton<E> {

    public static E getInstance() {
        if (instance == null)
            instance = new Singleton<E>();

        return instance;
    }

    private static E instance = null;
}

无法编译,因为不能使用 static 修饰泛型 E

通配符

泛型类型是固定的,某些场景下使用起来不太灵活,于是,通配符就来了! 通配符可以允许参数类型变化,用来解决泛型无法协变的问题。
举例:

// 限制类型为 Person 的子类
<? extends Person>
// 限制类型为 Manager 的父类
<? super Manager>

通配符和常用的泛型E之间的区别

  • E 可以用于声明变量或常量而*?* 不行
  • E 一般用于声明泛型类或方法,通配符 ? 一般用于泛型方法的调用代码和形参。
  • E 在编译期间会被擦除为限定类型或 Object ,通配符用于捕获具体类型。

无界通配符

无界通配符可以接收任何泛型类型数据,用于实现不依赖于具体类型参数的简单方法,可以捕获参数类型并交由泛型方法进行处理。

void testMethod(Person<?> p) {
  // 泛型方法自行处理
}

List< ? >和List的区别

  • List< ? > list表示list 是持有某种特定类型的List,但不知道具体是哪种类型,所以,添加元素进去的时候会报错。
  • List list 表示 list 是持有的元素类型是Object,因此可以添加任何类型的对象,只不过编译器会有警告信息。
List<?> list = new ArrayList<>();
list.add("sss");//报错
List list2 = new ArrayList<>();
list2.add("sss");//警告信息

上边界通配符,和下边界通配符

在使用泛型的时候,可以传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类
上边界通配符 extends 可以实现泛型的向上转型即传入的类型实参必须是指定类型的子类型。
举例:

// 限制必须是 Person 类的子类
<? extends Person>

类型的边界可以设置多个,还可以对E类型进行限制。

<E extends E1 & E2>
<E extends XXX>

下边界通配符super 于上边界通配符 extends 刚好相反,它可以实现泛型的向下转型即传入的类型参数必须是指定类型的父类型。
举例:

//  限制必须是 Employee 类的父类
List<? super Employee>

? extends xxx和? super xxx 的区别
两者的接收参数范围不同。并且,使用 ? extends xxx 声明的泛型参数只能调用 get() 方法返回 xxx 类型,调用 set() 报错。使用 ? super xxx 声明的泛型参数只能调用 set() 方法接收xxx类型,调用 get() 报错

E extes xxx 和 ? extends xxx的区别
E extens xxx 用于定义泛型类和方法,擦除后为xxx类型,? extends xxx 用于声明方法形参,接收xxx和其子类型。

Class< ? > 和 Class的区别
直接使用Class的话会有一个类型警告,使用Class< ? > 则没有,因为Class是一个泛型类,接收原生类型会产生警告

举例:

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

class Node<E> { /* ... */ }

Node<Circle> nc = new Node<>();
Node<Shape>  ns = nc;

不能编译,因为 * Node< Circle >* 不是 Node< Shape > 的子类

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

class Node<T> { /* ... */ }
class ChildNode<E> extends Node<E>{

}
ChildNode<Circle> nc = new ChildNode<>();
Node<Circle>  ns = nc;

可以编译,ChildNode< Circle > 是 *Node< Circle >*的子类

你可能感兴趣的:(Java面试准备,java)