Java面试:泛型程序设计基础

136 阅读8分钟

纯干货🤔,有点无聊,还没完全搞懂

基本概念

什么是泛型

  • 泛型就是将类型进行参数化,定义类或者接口或者方法的时候,指明参数类型是泛型,在使用的时候,需要指定具体的类型。比如创建ArrayList对象的时候,需要指定往集合中插入的元素的类型,Integer或者String或者自定义类型。

泛型的作用

  • 使用泛型避免了强制类型转换的情况;而且在编译期就可以检测到类型错误。
  • 使用泛型,提高了代码的复用性。比如开发项目中,自定义接口通用的返回结果,使用泛型可以往返回结果类中加入不同类型的返回值。

泛型类、泛型接口、泛型方法

泛型类

基本语法

public class BaseResult<T> {
    private String code;
    private String message;
    private T data;
    private Boolean isSuccess;
}

泛型方法

基本语法

<T> 表示声明这个方法是一个泛型方法(带有该标识符的才是泛型方法),T是这个方法的返回值,是已经在类中声明的类型变量,该方法接受一个泛型类Class<T> t

protected <T> T getTaskData(Object id, String id2, Class<T> t) {
    // 操作
    return ObjHelper.convertTo(A, t);
}

方法的返回值也可以是其他类型,如void:

protected <T> void getTaskData(Object id, String id2, Class<T> t) {
    // 操作
}

泛型方法可以定义在泛型类中,也可以定义在普通类中

public class ArrayAlg {
    public static <T> T getMiddle(T... a) {
        return a[a.length / 2];
    }
}

这是泛型方法接受两个泛型参数的传入,所以声明的时候使用了<S, T>

public static <S, T> T copy(S source, Sup<T> target) {
    T t = target.get();
    copy1(source, t);
    return t;
}

泛型接口

定义泛型接口

public interface Box<T> {
    void add(T item);
    T get();
}

// 实现泛型接口
public class IntegerBox implements Box<Integer> {
    private Integer item;

    public void add(Integer item) {
        this.item = item;
    }

    public Integer get() {
        return item;
    }
}

// 使用泛型接口
Box<Integer> box = new IntegerBox();
box.add(10);
Integer value = box.get();

实现泛型接口

  1. 具体类型参数实现:在实现类中显式指定具体的类型参数。这将使实现类只能处理特定类型的数据。
public class IntegerBox implements Box<Integer> {
    private Integer item;

    public void add(Integer item) {
        this.item = item;
    }

    public Integer get() {
        return item;
    }
}

在上面的示例中,IntegerBox 类实现了泛型接口 Box<Integer>,并明确指定了类型参数为 Integer。因此,IntegerBox 类只能处理整数类型的数据。

  1. 保留泛型类型参数:在实现类中继续使用泛型类型参数。这将使实现类具有与泛型接口相同的类型参数,从而保持灵活性。
public class GenericBox<T> implements Box<T> {
    private T item;

    public void add(T item) {
        this.item = item;
    }

    public T get() {
        return item;
    }
}

类型变量的限定

什么是类型变量的限定

有些业务场景使用的泛型方法中调用了特定类才有的方法,这时候就需要对类型变量进行约束,限制必须为某个类型;否则会报错(编译出错,还是运行出错?)。

举例子:比较两数的大小,使用了compareTo方法,但只有实现了Comparable接口的类,才能使用这个方法,所以限定T extends Comparable>

public class MinValue{
    public <T extends Comparable> T minValue(T number1, T number2) {
        if (number1.compareTo(number2) > 0) {
            return number2;
        }else {
            return number1;
        }
    }

    public static void main(String[] args) {
        MinValue min = new MinValue();
        System.out.println(min.minValue(1, 2));
    }
}

在ide中试了下,如果没有将T限制为extends Comparable,ide会直接出现红色(如下图),也就是编译时报错

image.png

类型变量限定的语法

通过extends关键字来限定类型变量。有两种限定方式

  1. 单一限定

限定类型必须为某个类或接口的子类。代码实战:

public <T extends TxBasicResp> T post(String url, JSONObject params, Class<T> clazz) {
    T result = restTemplate.postForObject(...);
    return result;
}
public abstract class MapperImpl <M extends BaseMapper<T>, T> extends ServiceImpl<M, T> {
    
}
  1. 多重限定

指定类型参数必须是多个类或接口的子类,并且只能有一个类(如果有)。

public class MyClass<T extends ClassA & InterfaceB & InterfaceC> { 
// 类成员和方法定义 
}

一个类型变量有多个限定类型时,使用“&”分隔;而逗号用来分隔多个类型变量。比如:

public class MyClass<T extends ClassA & InterfaceB, S extends Class A> { 
// 类成员和方法定义 
}

通配符(没写完,还不太懂)

通配符的作用

让泛型程序设计更加灵活。 举个例子:

1.有派生关系的两个类,放到泛型容器之后,无法互相引用(还没找到具体的例子,为什么要互相引用?

  • 定义一个Fruit类及其子类:Apple
class Fruit { };
class Apple { };
  • 定义一个未使用通配符的Plate泛型类
class Plate <T> {
    private T item;
    public Plate(T t) { item = t;}
    public void setItem(T t) { item = t;}
    public void getItem() { return item;}
}
  • 定义一个“水果盘”,逻辑上“水果盘”可以装苹果,但以下语句会报错(类型不兼容):
Plate<Fruit> p = new Plate<Apple> (new Apple());
  • 编译器认定的逻辑是这样的:苹果是一种水果,但是装苹果的盘子不是装水果的盘子
  1. 有派生关系的两个类,放到泛型容器之后,子类所在容器无法使用父类所在容器的方法
public class Pair<T> {
    private T first;
    private T second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst(){
        return first;
    }

    public T getSecond() {
        return second;
    }

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

    public void setSecond(T second) {
        this.second = second;
    }
}

// Employee类
public class Employee {
    Integer id;
    String name;

    public Employee(){

    }

    public Employee(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId(){
        return id;
    }

    public String getName() {
        return name;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }
}

// Manager类
public class Manager extends Employee {
    Integer id;
    String name;
    Double salary;

    public Manager(){

    }

    public Manager(Integer id, String name, Double salary) {
        this.id = id;
        this.name = name;
        this.salary = salary;
    }

    public Integer getId(){
        return id;
    }

    public String getName() {
        return name;
    }

    public Double getSalary() {
        return salary;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setSalary(Double salary) {
        this.salary = salary;
    }
}

// PrintBuddiesMain.printBuddies(managerPair)编译会报错
public class PrintBuddiesMain {
    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.");
    }
    public static void main(String[] args) {
        Manager manager1 = new Manager(1, "张三", 6000.00);
        Manager manager2 = new Manager(2, "李四", 7000.00);
        Pair<Manager> managerPair = new Pair<>(manager1, manager2);

        Employee employee1 = new Employee(1, "王无");
        Employee employee2 = new Employee(2, "周六");
        Pair<Employee> employeePair = new Pair<>(employee1, employee2);

        PrintBuddiesMain.printBuddies(managerPair);
        PrintBuddiesMain.printBuddies(employeePair);

    }
}

//以下是不报错的,改写printBuddies方法即可
public class PrintBuddiesMain {
    public static void printBuddies(Pair<? extends Employee> p) {
        Employee first = p.getFirst();
        Employee second = p.getSecond();
        System.out.println(first.getName() +  " and " + second.getName() + " are buddies.");
    }
    public static void main(String[] args) {
        ...
    }
}

通配符的类别(基础语法)

上界通配符

// 泛型类可以存放 Fruit 及其子类
class Plate <? extends Fruit> {
    // 方法体
}

Plate<? extends Fruit>Plate<Apple>最大的区别是:Plate<? extends Fruit>Plate<Fruit> 以及Plate<Apple>的基类。

image.png

下界通配符

class Plate <? super Fruit> {
    // 方法体
}

表达的就是相反的概念:一个能放水果以及一切是水果基类的盘子。

对应刚才那个例子,Plate <? super Fruit> 覆盖下图区域

image.png

无限制通配符

// 泛型方法接受任意类型的参数
void myGenericMethod(List<?> list) {
    // 方法体
}

上下界通配符的副作用

上界通配符

使用上界通配符的类<? extends anyClass> ,不能往对象里存东西,但可以从对象中获取内容。 而且取出来的东西只能放到‘anyClass’类或者其基类中。

比如:

Plate<? extends Fruit> p = new Plate<Apple>(new Apple());

p.set(new Fruit()); // error
p.set(new Apple()); // error

Fruit newFruit1 = p.get();
Object newFruit2 = p.get();
Apple newFruit3 = p.get(); // error

原因:编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道。 编译器在看到后面用Plate赋值之后,盘子里没有被标上有“苹果”。而是标识符:capture#1,表示捕获一个Fruit或者其子类。无论是往里插入Fruit还是Apple,都没有办法和capture#1匹配上,所以不允许插入

通配符和类型参数T的区别

根据上述分析,对编译器来说,只要使用类的之后指定了类型,那所有的T都代表同一种类型。但通配符没有这种约束,只知道类里面放了一个东西,具体是什么不清楚。

下界通配符

使用下界通配符Plate<? super Fruit>的泛型类,往对象里存东西可以,但是取出来的时候只能赋值给Object对象。

Plate<? super Fruit> p = new Plate<Fruit>(new Fruit());

p.set(new Fruit()); 
p.set(new Food()); 

Fruit newFruit1 = p.get(); // error
Apple newFruit3 = p.get(); // error
Object newFruit2 = p.get();

因为下届通配符,只限定了可以存入类型的最小范围,所以只要存的类是Fruit或者基类就行。取出来的内容,需要使用Object对象来存储,否则可能会报类型不兼容的错误?

PECS(Producer Extends Consumer Super)原则

1、频繁往外读取内容的,适合用上界。

2、经常往里插入的,适合用下界。

可参考文章:

1.腾讯云开发者社区:困扰多年的Java泛型 extends T> super T>,终于搞清楚了!

2.思否:Java泛型通配符 ? 与 T 的区别

3.阿里云开发者社区: 一文读懂Java泛型中的通配符?

类型擦除和桥方法

什么是类型擦除

Java程序编译期间,将泛型类型转换为非泛型类型。删去类型变量,使用它们的限定变量来替代,如果没有限定变量,则替换为Object类型。如下:

  1. 没有限定变量的情况
public class Pair {
    private Object first;
    private Object second;
    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }
    public Object getFirstO { return first; }
    
    public Object getSecondO { return second; }
    
    public void setFirst(Object newValue) { first = newValue; }
    
    public void setSecond(Object newValue) { second = newValue; }
}
  1. 有限定变量的情况

为什么要有类型擦除

类型擦除有哪些影响

参考文章:juejin.cn/post/724991…