记录《疯狂Java讲义精粹》划线内容及理解(第七章-泛型)

前言:《疯狂java讲义精粹》这本书一共十三章,现在我写到第七章,也算是已经过半,半程风雨半程春,从上一章的集合开始,乃至到第五章的面向对象(下),面对越来越多陌生的知识,我开始系统性的做笔记,很少有划线的内容,所以从现在开始,

        在代码块内的,是我在书中划线内容,而什么都不加的,就是我自己的笔记,并非书中原话

以上

第七章-泛型

7.1 泛型入门


java集合在放进元素之后会忘记元素的类型,这样设计的初衷是,设计java时不知道之后的程序员,会用集合来保存什么样的元素,所以要设计为能保存所有元素


如果将不同类型的对象丢进同一个list,在取元素强制类型转换时,会报ClassCastException异常

7.1.2 手动实现编译时检查类型


简单来说就是,自定义一个类,将ArrayList类组合进自定义类(作为私有变量),

之后重写add和get方法,传入的参数设置为String,这样在add方法体中使用ArrayLiat的add方法,加入集合的元素必定是String,相当于做了限定

重些的get方法传入int的索引值,使用ArrayLiat的get方法取到值之后再强转为String,就能实现元素类型的限定

这样做,可以实现加入集合的元素类型保持一致,且取元素是不用强转,但缺点是,如果使用这种方法,在程序中要写大量的子类,


为了限定集合元素类型,java在jdk5,引入了“参数化类型”的概念,也就是在定义集合时尖括号中的内容<int>,也称为泛型

7.1.3 使用泛型


使用格式

​ List<String> strList = new ArrayList<String>();

这样写的好处是:

​ 1.限定了集合内元素的类型

​ 2.取出元素时不需要强制类型转换

7.1.4 Java 7泛型的“菱形”语法


​ java在jdk7时,对泛型格式做了进一步精简,格式如下

​ List<String> strList = new ArrayList<>();

也就是说不需要在构造函数的尖括号内,限定元素类型了

需要注意的是,这里的菱形语法,在后面将会导致一个比较怪异的错误,所以谨慎使用

7.2 深入泛型


泛型的使用场景

​ 1.类

​ 2.接口

​ 3.方法

在定义这些的时候,可以指定类型形参(泛型),之后创建/调用对应的类/方法时,根据传入的参数,动态指定类型形参,这个指定的位置比较特殊,后面会详细解释

在类中接入泛型时,需要将泛型对应类型的变量,写为成员变量

在jdk5之后,java改写了集合框架中的所有接口和类,为这些增加了泛型支持

7.2.1 定义泛型接口、类


在定义关于类或接口的泛型时,位置在类名或接口名的后面

格式为

​ public interface test<E>{

​ ……

}

在类中的方法中,就可以使用 void add(E e)这种方法调用这个类型形参

在实际使用时,调用时传入的具体类型,会将E替换,这样就产生了很多“新”的list接口,但这些都只是在逻辑上存在的,在内存中还是只存在一个list源码


包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上并不存在。


我们可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要使用场所)


但构造器的参数部分,还是可以使用泛型的参数类型,作为形参

当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。

但是可以对构造器,进行泛型方法的改写,也就是在构造器上声明一个泛型,但是这样,将和泛型的棱形语法一起,导致一个错误

//构造器不需要使用泛型
public  class  Test<E>{

//构造器不需要增加声明泛型
Test{

……
}


//将构造器增加泛型方法声明
public<T> Test(T  t){
……

})


}

7.2.2 从泛型类派生子类


当定义的泛型类或接口,继承子类时,泛型的类型形参E,要具体为某一种具体的数据类型

如:

​ public class A extends Apple<String>

继承/实现,时也可不写类型参数

如:

​ public class A extends Apple

但如果要在子类中重写父类的方法,那么对应的参数类型(返回值,或者形参),也要具体为某一种类型

使用了未经检查或不安全的操作——这就是泛型检查的警告,

在这种情况下,系统会将<T>当作object来处理

7.2.3 并不存在泛型类


在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。


将list的类型参数限定为不同的类型,用getClass()方法取类名,做比较,会返回true,这说明泛型所约定的,这个”具体“的类并不存在,它只是一个逻辑上的概念

这也是上面,不允许在静态方法等等中,使用类型参数的原因,

因为这个类型参数T,是一个虚拟的概念,所以没办法划分内存空间

由于系统中不会真正生成泛型类,所以instanceof也不能用于泛型类,有些难理解,

如:

​ if(cs instanceof List<String>)

是错误的

7.3 类型通配符


这里有段代码

public void test(List c){
    for(int i=0;i<c.size();i++){
     System.out.println(c.get(i))   
    }
}

关于list的集合遍历,可以使用迭代器,或者for都可

 Iterator<E> 迭代器名 = 集合名.iterator();

复习以下迭代器的知识,

        ​ hasNext() 判断是否还有元素,返回布尔,一般用作循环条件

        ​ next() 获取集合中的元素,用作遍历

for(……)

        ​ 在for中,对于list一般使用get方法来遍历


这里说一个特例,如果使用ArrayDeque,做为栈,然后在for中使用pop来遍历队列元素

那么遍历出来的元素不正确,

这个问题是,pop会将拿到的数弹出栈,然后ArrayDeque的长度,size()方法的值会减小,但 i++还在向前,所以造成跳跃遍历输出,

解决方法: 使用 removeFirst() 获取对头元素,并删除


如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<Foo>并不是G<Bar>的子类型!这一点非常值得注意,因为它与我们的习惯看法不同。

如果存在,有继承关系的两个类a.b,则将这两个类作为泛型的类型实参时,a.b两类将失去继承关系

一门设计优秀的语言,不仅需要提供强大的功能,而且能提供强大的“错误提示”和“出错警告”,这样才能尽量避免开发者犯错


关于上面,a,b两个类作为泛型的参数类型时,两个泛型将不存在子父类关系

        ​ 原因是:这是泛型在设计时参考数组,而做出的设计优化,

​         数组允许,将一个integer数组变量,赋给一个Number数组变量,在编译时表现为Number的特性(允许小数加入数组),但在运行时表现为integer(不允许小数加入数组),这样会带来潜在的安全隐患

​ 所以泛型做出了改进,取消了两个类在泛型中的继承关系

7.3.1 使用类型通配符


在定义方法,或类,或接口时使用 <?> 表示,该参数类型,通过传入的值确定,扩展了程序的灵活性,

​              在定义集合时,不能使用类型通配符(也就是说,不能使集合内存储的元素类型不确定)

7.3.2 设定类型通配符的上限


使用格式:

                        ​ list<? entends 父类名>

当使用类型通配符上限作为,方法形参时,方法内不能做加入集合操作,

原因是:我们无法准确得知?代表那个类型,所以无法将任何对象加入集合

解决方案:这个问题在后面的泛型方法中,将会得到解决

7.3.3 设定类型形参的上限


在定义类和接口时,也可以设定上限,​              

   public calss Test<T extends Number>{

                                ​ ……

                }

该上限表示传入的类型,只能是Number,或者它的子类


一种极端情况是,设定多个上限(一个父类,多个接口)

                        ​ public class Test<T extends Number & java.io.Serializable>

这时传入的类型,必须是Number的子类,并且实现接口

                        ​ 这里格式要求,有多个上限时,类必须放在第一位

7.4 泛型方法


​ 无论类上有没有定义泛型(类型形参),方法中都可以使用泛型作为形参

为了解决,泛型通配符作为方法形参时,方法无法向集合加入元素这一问题,java在jdk5,加入了泛型方法

格式:                     

   ​ static<T,S> void addList(T[] a,collection<T> c){

                                                ​ ……

                        }

​ 泛型方法在调用时,不需要显示传入 类型参数,编译器会自己推断出类型形参的值

如:

​ Integer[] a=new Integer[5];

​ collection<Integer> b=new collection<>();

​ addList(a,b)

可以看出不需要显示指定类型参数的值

7.4.2 泛型方法和类型通配符的区别


泛型方法和类型通配符都可以用作,定义方法的形参,选择的区别在于

类型通配符可以灵活的表示数据类型

泛型方法可以表示方法一个或多个参数之间的依赖关系

两者还可以同时使用,比如java源码中的Collections中的copy方法


类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的类型形参必须在对应方法中显式声明。

7.4.3 Java 7的“菱形”语法与泛型构造器


泛型构造器,就是在一个类的构造器,的修饰符后面加上参数类型

​ public <T> Foo(T t){

​ System.out.println("t")

}

调用该构造器

1.new Foo(200)                                 构造器中的T为Integer类型

2.new <String> Foo("wawawa")        显示指定类型参数,给的值和指定的参数用以类型,编译正确

如果使用泛型构造器,且类上加了参数类型,将限制菱形语法,

比较复杂,例子如:

class MyClass<E> {
        public <T> MyClass(T t) {
​
            System.out.println("t参数的值为:" + t);
​
        }
​
    }
​
    public class GenericDiamondTest {
        public static void main(String[] args) {
​
// MyClass类声明中的E形参是String类型
​
//泛型构造器中声明的T形参是Integer类型
​
MyClass<String> mc1 = new MyClass<>(5);
​
//显式指定泛型构造器中声明的T形参是Integer类型
​
            MyClass<String> mc2 = new <Integer>MyClass<String>(5);
​
// MyClass类声明中的E形参是String类型
​
//如果显式指定泛型构造器中声明的T形参是Integer类型
​
//此时就不能使用“菱形”语法,下面代码是错的
​
//MyClass<String> mc3 = new <Integer> MyClass<>(5);
​
        }
    }

简单来说,如果使用了泛型类<E>,泛型构造器<T>,并且二者的类型不同,那么,在调用构造器时,直接使用

​ 类名<E> 实例名 = new <T>构造器名<E>(T t);

7.4.4 设定通配符下限


格式:

                                      ​ <? super E>

问号表示的元素,必须是类型E,或者E的父类

java源码中的TreeSet的一个构造器就使用了这种语法,要求传入一个comparator的定制排序类,

这个排序类的底线,就是TreeSet的元素类型,也可以向上,是这些元素的父类

7.4.5 泛型方法与方法重载


泛型既允许设定通配符的上限(<? extends T>),也与允许设定通配符的下限(<? super T>)

那么这里会出现一种极端情况,假设一个类中有同名的两个方法(方法重载),每个方法都有两个变量,但就是一个方法的参数用上限,一个方法的参数用下限,

这种非常极端的情况,如果只是存在,编译阶段不会报错,不过一但调用这个方法时,编译器将会报错,因为对于传入的数据,两个方法都可以执行,所以编译器不确定你想用那个

这个极端的例子,如想仔细了解,看书这一节的例子

7.5 擦除和转换


在泛型的定义中,类和方法总是带着类型参数的(因为他是泛型吗),但是为了与老旧java代码保持兼容性,所以java也允许,使用泛型类时,不指定类型参数,

​ 那么这时的参数类型,默认是,指定该泛型类型时,指定的第一个上限类型(object),

​ 称为——raw type(原始类型)

2022.08.12更新

补充以下,raw  type是java为了兼容古老版本而做出的妥协,

如果使用泛型类,不指定泛型的具体类型,则该泛型的类型会被认为object

当把一个具有泛型信息的对象,赋值给另一个没有泛型信息的对象时,所有在尖括号中的内容都将被丢掉,比如将List<String>转为List,则集合中的所有元素的类型变为,Object类型

​ 这种情况,称为  #擦除#

转换——java允许将一个没有泛型信息的对象,直接赋给有泛型信息的对象,编译器会提示,“未经检查的转换”

这里有种极端情况,假设将一个list<integer>,先擦除,给一个list,再转换,给list<String>,编译器报“未经检查的转换”

​          这个代码不会报错,但一但操作list<String>,将会出错,因为这个list内部,并不是String,我猜测擦除之后它的,类型是object,或许是它原来的类型,integer

7.6 泛型与数组


泛型有一个设计原则,就是编译时没有报“未经检查的转换(uncheckd)”,则运行时不会引发classcastException异常

具有泛型的集合类型,可以声明数组变量,但不可创建实例(不能创建泛型数组)

但java允许创建无上限的通配符泛型数组

如:

​ List<?>[] s=new ArrayList<?>[10];

不能创建元素类型是,一个类的某个属性的数组对象

如代码:

new T[coll.size()]

上面这行代码将引发错误

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值