Java 范型基础

650 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

什么是范型?

泛型程序设计(generic programming)是程序设计语言的一种风格范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。

泛型的定义及目的

泛型的定义主要有以下两种:

  1. 在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表类,不能代表个别对象。(这是当今较常见的定义)
  2. 在程序编码中一些包含参数的。其参数可以代表类或对象等等。(现在人们大多把这称作模板

不论使用哪个定义,泛型的参数在真正使用泛型时都必须作出指明。

一些强类型程序语言支持泛型,其主要目的是加强类型安全及减少类转换的次数,但一些支持泛型的程序语言只能达到部分目的。

Java 中的泛型

Java 泛型的参数只可以代表类,不能代表个别对象。由于 Java 泛型的类型参数之实际类型在编译时会被消除,所以无法在运行时得知其类型参数的类型,而且无法直接使用基本值类型作为泛型类型参数。Java 编译程序在编译泛型时会自动加入类型转换的编码,故运行速度不会因为使用泛型而加快。

由于运行时会消除泛型的对象实例类型信息等缺陷经常被人诟病,Java 及 JVM 的开发方面也尝试解决这个问题,例如 Java 通过在生成字节码时添加类型推导辅助信息,从而可以通过反射接口获得部分泛型信息。通过改进泛型在 JVM 的实现,使其支持基本值类型泛型和直接获得泛型信息等。

范型的存在几种形式

普通代码的实现

public class Box {
   private Object object;

   public void set(Object object) { this.object = object; }
   public Object get() { return object; }
}

范型方式的实现

public class Box<T> {
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

一些常用的泛型类型变量:

  • E:元素(Element),多用于java集合框架
  • K:关键字(Key)
  • N:数字(Number)
  • T:类型(Type)
  • V:值(Value)

接口中存在范型

我们可以定义一个 ICall 带范型的接口

public interface ICall<T> {
    T exec(Param param);
}
  1. 我们可以定义这样的一个实现类来实现 ICall ,如果我们在定义 MethodCall 类的时候不申明范型

MethodCall implements ICall<T> 在编译的时候会提示编译错误:

Error:(6, 43) java: 找不到符号 符号: 类 T

public class MethodCall<T> implements ICall<T> {

    @Override
    public T exec(Param param) {
        Object object = new ArrayList<>();
        return null;
    }
}
  1. 当实现范型接口类传递具体的类型的时候,这个时候接口方法也应该修改为具体的类型实现。如:
public class MethodCall<String> implements ICall<String> {

    @Override
    public String exec(Param param) {
        Object object = new ArrayList<>();
        return null;
    }
}

类中存在范型

jdk 最常见的如:List、Set、Collection 等集合类

public class MethodCall<T> {
    
    public T exec(Param param) {
        return null;
    }
}

注意:

  • 泛型的类型参数只能是类类型,不能是基础类型(boolean、byte、char、short、int、long、float、double)。
  • 不能对确切的泛型类型使用 instanceof 操作。如下面的操作是非法的,编译时会出错。
Object object = new ArrayList<>();
if (object instanceof List<String>) {

}

静态方法中范型

public class JsonUtil {

    public static <T> T toJson(String jsonStr, Class<T> clazz) {
        try {
            T t = clazz.newInstance();
            // todo ...
            return t;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
}

普通方法中范型

public class MethodCall<T> {

    public T exec(Param param) {
        return null;
    }
}

泛型继承和子类型

不支持范型继承如

List<Number> list = new ArrayList<>();
list.add(new Integer(1));
list.add(new Double(0.1));

如果修改 list 的话是不支持的

list = new ArryList<Integer>(); // 编译出错

类型推断

类型推断是 Java 编译器查看每个方法调用和相应声明以确定使调用适用的类型参数的能力。推理算法确定参数的类型,以及确定结果是否被分配或返回的类型(如果有)。最后,推理算法尝试找到与所有参数一起使用的最具体的类型。

为了说明最后一点,在下面的示例中,推论确定传递给pick方法的第二个参数的类型为Serializable

static <T> T pick(T a1, T a2) { 
  return a2; 
}

Serializable s = pick("d", new ArrayList<String>());

实例化类型推断

如:

Map<String, String> map = new HashMap<>();

构造方法中类型推断

class<T> MyClass {
  private T t
  public MyClass(T t) {
    this.t = t;
  }
}

MyClass myClass = new MyClass("a");

类型擦除

Java 语言引入了泛型,以在编译时提供更严格的类型检查并支持泛型编程。为了实现泛型,Java 编译器将类型擦除应用于:

  • 如果类型参数不受限制,则将通用类型中的所有类型参数替换为其边界或对象。因此,产生的字节码仅包含普通的类,接口和方法。
  • 必要时插入类型转换,以保持类型安全。
  • 生成桥接方法以在扩展的泛型类型中保留多态。

类型擦除可确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。

案例

  1. 案例一
List<String> list1 = new ArrayList<String>();
List<Integer> list2 = new ArrayList<Integer>();
System.out.println(list1.getClass() == list2.getClass());

// 打印 true

参考文档

docs.oracle.com/javase/tuto…

baike.baidu.com/item/%E6%B3…

docs.oracle.com/javase/tuto…

www.sohu.com/a/245549100…