八. (《Java核心技术》读书笔记+重点整理系列)泛型写法和底层实现

泛型作用

为了代码可以被不同类型的对象所重用。在泛型出现以前,类似的功能借助Object类来实现,使用的时候使用强制类型转换调用。比如ArrayList类:

public class ArrayList{
	Object[] elementData;
	...
	public Object get(int i){}
	public void add(Object o){}
}
public static void main(String[] args) {
	String name = (String)list.get(0);
	list.add(new String(""));
}

这样带来很多问题,由于add的时候没有进行类型检查,可以将任何类型的对象加入列表。在get的时候,如果类型不支持转换就会报错。所以在java中引入了泛型,带来了更好的可读性和安全性。

泛型写法

泛型类

//写在类名后面,通常用KV、TUS表示,在类中可以当作正常类使用
public class Main<T, V>{
    private T name;
    private V age;

    public Main(T user, V i) {
        name = user;
        age = i;
    }

    public T getName() {
        return name;
    }
    public V getAge() {
        return age;
    }

    public static void main(String[] args) {
        Main<String, Integer> main = new Main<>("user", 12);
        System.out.println(MessageFormat.format("用户名:{0} 年龄:{1}", main.getName(), main.getAge()));
        //MessageFormat.format用占位符占位,性能更优
        //String.format用正则表达式占位,两者功能一致
    }
}

泛型方法

//方法签名中写在返回类型前,调用是写在方法名前
public class Main{
    public <K, V> void getUser(K k, V v) {
         return;
    }

    public static void main(String[] args) {
        Main main = new Main();
        main.<String, Integer>getUser("user", 12);
        //类型可以省略,编译器有足够的信息推断出泛型的实际类型
        //当只有一个泛型类,有多个参数时,编译器推断所有参数的公共父类为泛型类,如果没有公共则报错。
    }
}

泛型变量限定

泛型中,可以用extends关键字对类型变量进行限定。在仔细学习之前,我一直认为这里的extends就是通配符,我还纳闷书里为什么会把这俩内容分开写。但是反复阅读实践后发现,变量限定用在泛型类定义中,通配符用在泛型类使用中,下面详述一下泛型变量限定。

使用最简单的泛型类时,发现在类或者方法中,我们不能对该类型的变量做出任何的操作(除了Object类中方法,至于为什么会在后文解释)。

//在设计的时候我们知道C类接受的都是集合类,我们想在C中访问一些集合类的方法,这个时候简单的泛型类就无法胜任。
public static class C<T>{
    T c;
    public void compute(){
        c.toString();
        //c.size();
    }
}
//当使用extends限定时,就可以使用父类的所有方法等
public static class C<T extends List>{
    T c;
    public void compute(){
        c.toString();
        c.size();
    }
}

这里的限定不光可以是类,还可以是接口,还可以同时有多个限定用&隔开。但需要注意的是,只能有一个类且必须要写到第一个位置,这与泛型类在虚拟机中的处理有关,下文会解释。

通配符

即便是引入了泛型的限定,但在泛型类或泛型方法定义的那一刻,泛型已经变成了一个既定的具体类。这种固定的泛型系统在灵活性上还是会有所欠缺,那么有没有一种方法,在定义之后仍然是多态的,这就要用到通配符。
先看一个实际的应用

boolean addAll(Collection<? extends E> c);
//这个是List类中的addAll方法,这里就很灵活,可以合并原有类型的子类型集合

除了子类型限定extends关键字外,还有超类型限定super关键字。一个是表明泛型类上界,一个是下界。因为定义上的差别,也导致了使用时两者的差别。用一句话来说,就是super可以向泛型对象写入,extends可以从泛型对象读取,而归根结底是因为java中多态特性,子类型可以赋值给父类型的特点。

public class Main{
    public static class A<T>{
        T t;
        public T get(){
            return t;
        }
        public void set(T t){
            this.t = t;
        }
    }

    public static class B{
        public void add(){

        }
    }
    public static class B1 extends B{

    }

    public static void main(String[] args) {
        A<? extends B> a = new A<>();
        B b = a.get();
        //a.set(new B());
        //extends时写入报错,因为在向对象内的泛型对象赋值,而对象内的类型是子类型。
        //同样当用super时get报错,set可以执行。
    }
}

如果取值传值想同时使用的话,可以使用<?>通配符。这里书中还有一些内容,比如子类型赋值给父类型的冲突,通配符其他用法,看来看去还是好复杂就不详述了。

虚拟机中的泛型代码(底层实现)

在JVM虚拟机中是没有泛型对象的,所有对象都必须是具体的普通类。也就说泛型的处理是发生在编译的过程中,编译后得到的字节码中不含泛型。

类型擦除

是指在编译阶段,将泛型类型替换为原始类型。原始类型指的是Object类,如果有限定就使用第一个限定类(这也是为什么在extends后要把类放在接口前面)。这是在泛型类内部发生的,而在泛型类外会直接隐去泛型,只保留类名。

泛型翻译

对象在处理的过程中,会使用原始类型进行处理。但对外表现的仍是用户指定的类型,这中间就需要用到类型转换。而泛型的作用,就是自动的、隐式的、安全的完成类型转换的工作。

public class Main{
    public static class A<T>{
        T t;
        public T get(){
            return t;
        }
    }
    //编译过后会把所有的T用Object替换,这是类型擦除

    public static void main(String[] args) {
        A<String> a = new A();
        String str = a.get();
        //会先以原始类型方法进行调用,然后将返回的Object类型转换为String。
        //也就说编译器会将这一条函数调用,翻译成以上两条字节码
    }
}

还有一个需要注意的点,在泛型函数中,如果有父类是泛型类,子类继承的情况时。会出现函数重载的情况,但我们本意是用子类函数重写父类函数。出现这类情况的原因是,父类类型擦除后实际参数类型是Object,子类参数类型是具体类,没有重写覆盖到,反而成了重载。这样就会出现调用的一些错误,解决方法可以使用桥方法。桥方法我也没搞透,详细的可以去书中研究。

引用检查

既然泛型类中使用Object类,那在平时的编写中,比如如下情况,又为什么会报错呢。

public class Main{
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        //list.add("1");报错
    }
}

这是因为编译器在编译前会对类型进行检查,检查无误后,在进行类型擦除。这里还有一个点需要注意,借用某位博主的总结。

public class Main{
    public static void main(String[] args) {
        ArrayList list = new ArrayList<Integer>();
        list.add(1);
        list.add("1");//这里并不会报错
    }
}

也就说java在设计的时候,是对引用进行类型检查。这里的引用中并没有声明类型,所以检查不出错误。直接加入到原始类型的List中去了。

泛型注意事项

使用约束

  • 不能使用基本类型作为泛型参数
  • 程序运行中,获取泛型类的类型,之和类型名有关(原因就是底层实现)
  • 泛型类不能有数组,而且泛型参数也不能是数组
  • 泛型不能实例化

这里只提取了几个常用的,具体原因都与泛型实现方法有关,具体原因不展开,书里有。

继承规则

T是S的子类,但List<T>和List<S>之间没有任何关系。

反射的利用

对于泛型类型,由于会进行类型擦除,获取不到太多的信息。而反射提供了在运行时分析任意类和对象的功能,那么可以借助反射获取泛型类信息。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值