JAVA基础之九-泛型(通用类型)

总体而言,泛型(通用类型)是一个好东西,它是一个工程上起到好处的东西,对于性能、安全等并没有什么帮助。

在java工程上,泛型属于必须掌握的,理由如下:

1.各种源码中基本上都有泛型,无论是java基础源码还是Spring或者阿帕奇的,不掌握这个,你读不懂。你没有方法绕过它

2.有了泛型,某种程度上会让代码更清晰和简洁

 

注意:本文中许多地方“泛型”和“通用类型”交叉使用,其中后者居多,二者都是表示java中的Generic Type。

通用类型,不是表示该类型通用,可以用用于任意地方,而是表示类的参数类型是不定的。

一、泛型定义

在oracle的官方文档中的描述:

A generic type is a generic class or interface that is parameterized over types

具体页面地址:https://docs.oracle.com/javase/tutorial/java/generics/index.html

Generic有通用,一般的意思。

其实翻译为通用类型也许更妥当一些,或者可参数化类型。

以上的一句话的意思:通用类型是允许参数化类型的类/接口

反过来的意思就是:如果一个类/接口不允许对其属性类型进行参数化处理,那么就不是通用的。

 

Java泛型是在Java 5(也称为JDK 1.5)中首次引入的,这一版本在2004年发布。泛型的引入是Java编程语言的一个重要里程碑

它允许程序员在编写类、接口和方法时指定类型参数,使得编译器可以在编译时检查类型安全,从而避免了类型转换异常,提高了代码的可读性和可维护性。
泛型的主要特性包括:
    类型参数化:允许类或接口在定义时指定一个或多个类型参数,这些类型参数在实例化时会被具体的类型所替换。
    类型检查:在编译时进行类型检查,确保类型安全。
    类型推断:从Java 7开始,引入了菱形操作符<>,简化了泛型实例化的语法,并且编译器能够自动推断泛型类型。
    泛型方法:可以在方法级别上定义泛型,而不仅仅是在类级别上。
    有界类型参数:通过extends和super关键字对类型参数进行限制,确保类型参数是某个特定类或接口的子类型或超类型。

自Java 5引入泛型以来,Java的后续版本(如Java 6、Java 7、Java 8等)对泛型进行了进一步的完善和增强。

例如,Java 7引入了菱形操作符,简化了泛型实例化的语法;Java 8增强了泛型的类型推断能力。这些改进使得Java的泛型更加易用和强大。
总的来说,Java泛型的引入是Java编程语言发展的一个重要里程碑,它极大地提高了Java代码的类型安全性和可维护性。

以上内容是文心一言总结出来的,认真看了下,没有什么毛病,注意几点:

1.重要里程碑-考虑到通用类型在java的源码中如此常见,可以肯定的这是名副其实

2.类型推断、编译器- 是的,实际干活的主要是编译器,不是运行时也不是编码时候,工程师只要关注其优缺点和使用场景即可

 

通用类型的几个关键词/符号:T,?,<>,extends,super

其中T(也可以是任意合法的字母、词汇,例如 GOGO,DODO,P,X,Y之类的)表示类型,表示特定类型的占位符,在实例化对象的时候需要指定T的具体类型。

其中?是占位符,表示任意类型(有一定限制),通常和extends/super组合使用,也可以单独使用(多在方法代码区域)

 

它们的组合可以是:

  1. <T> 表示不定类型,可用于类或者方法中,这是通用类型的最常见的使用形式。如果用在方法返回部分,但不构成“<T> T”,则毫无意义
  2. <T> T  ,表示返回类型的强制转换,通常用于方法
  3. T 通常用于方法参数或者表示返回类型
  4. <?> 表示不定类型,通常用于方法
  5. ? extends T  ,表示T的任意子类,通常用于方法
  6. ? super T,表示T的任意父类(祖类...),通常用于方法
  7. T extends E,表示T是E的任意子类,可以用于方法和类
  8. T super E,表示T是E的父祖类,主要用于方法

以上几种都必须牢记,建议通过java自有代码和自己编写例子来强化记忆。

 

如果不想编写过于复杂的功能,那么在日常的CRUD,大概只需要记住1、2、3 三种最为常见和典型的用法。

含super或者extends主要见于java或者sprng的代码中,在CRUD比较少那么写。

 

需要特别注意的是,组合虽然多,但是还是有许多限制的。 通用类型的实现全靠JCP解释,所以为什么这个行,那个

不行,并没有太多的理由,就好像为什么有引力。但随着java版本的迭代,通用类型有望得到更好的支持。

 

通用类型本质上在于:

1.限定/泛化方法的参数类型  -用在方法上,通常是限定参数类型

2.限定/泛化类成员的类型 -用在类上,通常是为了限定成员类型

 

二、通用类型经典例子(list)

2.1 ArrayList

以下是ArrayList的几个典型定义,包含了类的定义,其中5个是方法上的。

1.public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable;
2.public E get(int index);
3.public ArrayList(Collection<? extends E> c);
4.public void forEachRemaining(Consumer<? super E> action);
5.public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    };
6.public boolean removeAll(Collection<?> c);

这里都是经典的用法:限定类的特定类型/类型范围。

 

有一个稍微特殊的例clone()(上文第5):

ArrayList<?> v = (ArrayList<?>) super.clone();

考虑到返回的是Object类型,v定义为任意类型都是可以的,例如 ArrayList<E> v。

那么JCP这里为什么要写成<?>而不是<E>?

2.2、其它

-- jdbcTemplate
1.public <T> T queryForObject(String sql, Class<T> requiredType) throws DataAccessException {
        return queryForObject(sql, getSingleColumnRowMapper(requiredType));
}
2.public <T> List<T> queryForList(String sql, Class<T> elementType) throws DataAccessException {
        return query(sql, getSingleColumnRowMapper(elementType));
}

-- Stream
<R, A> R collect(Collector<? super T, A, R> collector);
public final Stream<P_OUT> limit(long maxSize) {
        if (maxSize < 0)
            throw new IllegalArgumentException(Long.toString(maxSize));
        return SliceOps.makeRef(this, 0, maxSize);
    }
-- jdk.internal.vm.vector.VectorSupport

@IntrinsicCandidate

public static

<VM ,Sh extends VectorShuffle<E>, E>

VM shuffleToVector(Class<?> VM, Class<?>E , Class<?> ShuffleClass, Sh s, int length,

ShuffleToVectorOperation<VM,Sh,E> defaultImpl) {

assert isNonCapturingLambda(defaultImpl) : defaultImpl;

return defaultImpl.apply(s);

}

在java的核心代码中,类似VectorShuffle这样让人头痛的代码还是不少的。

 

JdbcTemplate对于通用类型的使用都是相对标准和克制的。

但是在Stream中,通用类型得到了大量的使用,某种程度上可以说,没有通用类型,java这个Stream是编不出来的。

 

三、作用

注意:以下所阐述的内容都是局限于JAVA17版本为止,不能在21等可用版本上确认同样的情况。

3.1、主要作用

关于用途,官方文档给出的是三大点:

https://docs.oracle.com/javase/tutorial/java/generics/index.html

    Stronger type checks at compile time.
    A Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. 
Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find. Elimination of casts. The following code snippet without generics requires casting: List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0); When re-written to use generics, the code does not require casting: List
<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); // no cast Enabling programmers to implement generic algorithms. By using generics, programmers can implement generic algorithms that work on collections of different types, can be customized, and are type safe and easier to read.

 注:为了节省篇幅,对原文做了一些裁剪。

1.编译时候的类型检查

2.消除类型转换 -也就是有些人说的类型擦除,类型替换(为实际类型)

3.编写通用算法

这些是经典的作用,也是最主要的作用。

3.2、局限性

它有众多局限性,这些局限性可以归纳为一点:不能任意地在类定义、方法和变量中随意地使用通用类型相关的符号

1.不能在方法的参数中这样定义--方法中的通用类型符号只能跟在具体的类型之后用于限定某个类型

do(? extends T xx)

do(<? extends T> xx)

换言之,方法中参数,通用类型符号只能跟在具体类型之后,T除外。

2.在实例化变量的时候,使用通用类型语法,可能不会编译错误,但是可能会产生其它错误

List<? extends Cup> bookList=new ArrayList<>();

这样虽然不会报错,但是add方法无法使用,因为编译器无法确认 add方法实例的类型。

编译器老是提示add的参数必须是 ? extends Cup类型,但是即使你用一个Cup的子孙(WaterCup是Cup的儿子)也无法达成目的。

 

3.其他

例如如下示例是现阶段不支持的:

public class blade<T>{
    public void split( ? extends T){
    }
}

split的方法希望只支持特定的T子类,但是现阶段不允许这么写。


四、泛型的编译时和运行时

4.1保证编译时候不会出现问题

当我们完成一个通用类型后,就需要使用它。所谓的使用,就是在某个代码段中定义一个类的实例,或者使用实例方法(或者类的静态方法)

4.1.1定义通用类型的变量

工程师通常必须在变量中确定变量的参数类型,上文的类型是String。

以ArrayList为例子:

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

但是,在极少数的情况下,参数类型可以使用通配符,例如ArrayList.clone方法就是。

如果使用通配符,那么限制有很多,具体自行体会。

4.1.2使用通用类型的方法

为了便于说明,举例:

例1.public <T> T doThing(xxx)

例2.publicT doSomeThing(xxx)

这二者主要区别在于:

a.例1中<T> 会把返回值强制转为变量类型,这个<T>指的是变量的类型

如果不小心使用,则常常会发生编译不报错,但是运行时报错的情况(通常是形如"...cannot be cast to class....")

b.而例2则相反,它会要求变量必须是T类型的

如果不是T类型则会在编译时候报错。

4.2编译后的代码

如前,我们知道如何在编译的时候,避免通用类型的编译错误。

考虑到通用类型是为了支持各种类型,那么当一个通用类型被实例化后,其实例执行的代码其实还是类的代码,那么又如何保证能够使用正确的类型?

猜想:

a.在编译后的代码中,jcp在类代码中插入变量p用于保存外部传递进来的变量v的实际类型T,并且在运行时,把V==T,之后再进行类型转换

b.在编译后的变量中,类型被定义为Object,然后在使用的时候再进行强制转换

为了验证这个想法,写了一个最简单的例子:

public class GenericSimpleTest<T> {
    T value;
    public GenericSimpleTest(T value) {
        this.value = value;

    }
    public T getValue() {
        return value;
    }

    public static void main(String[] args) {
        GenericSimpleTest<String> test = new GenericSimpleTest<>("Hello World");
        System.out.println(test.getValue());
    }
}

 

使用javap查看

generictype>javap -l -c GenericSimpleTest.class

也可以使用Asm Bytecode viewer、Jadx或者 Bytecode viewer(eclipse),前二者是在idea中。虽然jcp不够方便,但是不用额外占用ide的资源,也不用额外安装,使用的频率也极低。

当然这些ide插件的优点是方便。

 

先说结论:就测试类GenericSimpleTest而言,是基于第二中方案实现(内部用Object存储,使用的时候进行强制转换)

注意看下上图标出的5点,以下逐一解释:

1.构造函数赋值,对应this.value=value,可以看到是Object类型

2.本地变量表中有一个变量指向类的变量value,类型是Object

3.方法getValue取回的值是Object类型

4.静态测试主类main中,在打印前会先检验getValue的的值是否为String类型,并尝试进行转换

5.main调用println打印字符串

 

指令checkcast的意思:尝试进行转换,否则返回null。

关于字节码中的指令,有几个可以参考的地址:

https://www.cnblogs.com/tsvico/p/12708417.html 

https://www.cnblogs.com/chanshuyi/p/jvm_serial_05_jvm_bytecode_analysis.html   (此人的jvm系列写得不错

https://blog.csdn.net/feiqipengcheng/article/details/109958051

常见的指令还是需要背诵下,否则看jcp比较吃力,好消息是指令的名称还是比较通俗易懂,哪怕不看专业参考也大概猜测出来。

字节码指令,如果只是做一些业务开发(非工具开发),通常没有必要精通,绝大部分工程师只要读源码的时候,知道指令的大概意思即可。

不过以我本人的经验而言,作为一个高级工程师还是有必要知道这些,至少理解java原理和阅读源码不会那么吃力。

 

五、我的例子

限于时间,自己直接编写一个例子,而不是查看有关程序的源码。

以下源码定义了一个ArrayList的子类,主要演示了定义方法的第三种情况:当使用了不在类上定义的泛型

public class GTypeTest2 {
    public static class MyArrayList<T> extends ArrayList<T> {
        public MyArrayList clone() {
            MyArrayList<T> list = (MyArrayList<T>) super.clone();
            return list;
        }

        @Override
        public T get(int index) {
            return super.get(index);
        }

        public <T> T getT(int index) {
            return (T) this.get(index);
        }

        public void addHuman(Human c) {
            add((T) c);
        }

        /**
         * 添加一个泛型方法,返回值是k. k不是在类上定义的泛型,所以必须在方法名前使用<K>
         * 这样可以表示它是一个泛型,否则会产生编译错误
         */
        public <K> K  addSp(K k) {
            add((T) k);
            return k;
        }

    }

    public static void main(String[] args) {
        MyArrayList<Object> list = new MyArrayList<>();
        list.add("hello");
        Human h = new Human("lzf");
        list.addHuman(h);
        //演示 “T” ,“<T> T”的截然想法的效果.
        //前者作用与编译时;后者作用于运行时

        //<T> T 测试-方法getT会在运行时试图把结果转为Human类型
        Human h2 = list.getT(1);
        System.out.println(h2);
        //这个是必然报错的测试
        try{
            House h3 = list.getT(1);
            System.out.println(h3);
        }
        catch (Exception e){
            System.out.println("异常类名称:"+e.getClass().getName());
            System.out.println("异常类信息:"+e.getMessage());
        }
        //T 测试
        Object o = list.get(0);
        System.out.println(o);

        //<K> K 测试  -- 注意k不是在类中定义的泛型,所以必须在方法名前使用<K>
        Woman w=new Woman();
        w.setName("mgy");
        
        Woman g=list.addSp(w);
        System.out.println(g.getName());
    }
}

 

特别注意下上文中红色字体部分。

运行main后,输出如下:

Human [gender=lzf, birthDay=null, power=null]
异常类名称:java.lang.ClassCastException
异常类信息:class study.base.oop.classes.Human cannot be cast to class study.base.oop.classes.House (study.base.oop.classes.Human and study.base.oop.classes.House are in unnamed module of loader 'app')
hello
mgy

 

六、小结

a.通用类型属于java工程师必须掌握的,如果希望写复杂一些内容。此外也有助于阅读源码

b.通用类型不是万能的,定义的时候存在各种限制。一些限制也许会在将来的版本中消除掉,规则是人定的,只要愿意,可以随心而定。

现在不支持的,将来就可能支持;反之,现在支持的,将来可以过时了。 如何解释,全看JCP的意思。

c.通配符?和T是不一样的,前者基本需要结合extends ,super使用,也可以单独使用,但是只能用于方法,典型的可以参考ArrayList.clone

d.通用类型可以出现在类、方法、方法体中。在做类型强转的时候,目标类型可以带通用类型符号。

e.如果一个表示通配符的符号K,在类中没有定义,但是希望在方法中使用,则必须在方法名前添加<K>,否则编译器不会当做不知道的类型,从而引发编译错误

f.如果想透彻理解泛型,有必要掌握一些javap和字节码的知识

 

posted @ 2024-10-10 13:56  正在战斗中  阅读(122)  评论(0编辑  收藏  举报