1、枚举类型

1.1 枚举类型简介

枚举类型是一种基本数据类型,使用枚举类型,可以取代使用final static定义的常量,即将常量封装在类或者接口中,同时枚举类型还赋予程序在编译时进行检测的功能。放在枚举类型里面的常量是没有具体的值的,定义一个枚举类型使用enum关键字。

  1. package MyPackage_1;
  2. public enum Constants {
  3. CONSTANTS_A,
  4. CONSTANTS_B,
  5. CONSTANTS_C,
  6. CONSTANTS_D
  7. }

当方法以枚举常量作为参数时,只能使用已经定义好的枚举常量,不能使用非枚举常量,这就是枚举类型的检测功能,避免无意义的程序。
image.png

image.png1.2 枚举类型常用的方法

枚举类型有四个常用的方法:
values():该方法可以将枚举类型的成员以数组形式返回;
valueOf():该方法可以实现将普通字符串转换为枚举类型,但是str的内容一定要和枚举类型的成员一样,否则会抛出异常;
compareTo():该方法用于比较每个枚举对象在定义时的顺序;
ordinal():该方法用于得到枚举成员的位置索引;

  1. package MyPackage_1;
  2. public class Test {
  3. public static void main(String[] args) {
  4. Constants[] array =Constants.values();
  5. for(Constants constants:array){
  6. System.out.println(constants);
  7. System.out.println(constants+"和"+Constants.CONSTANT_B+"的比较结果:"+Constants.CONSTANT_B.compareTo(constants));
  8. System.out.println(constants.ordinal());
  9. System.out.println("----------------");
  10. }
  11. System.out.println(Constants.valueOf("CONSTANT_C"));
  12. }
  13. }

运行结果:
image.png

2、泛型简介

2.1 泛型出现背景

在jdk1.5之前,集合容器类在声明阶段不能指定这个容器存放的是什么类型的对象,所以只能把容器的对象类型指定为object,但是这种旧的处理方式是有较大的缺点的:任何类型的对象都可以添加到容器中,没有对象类型的约束;而在取出容器中对象的时候只能获取到Object类型的对象,想获取到具体的对象类型则需要强转,这容易出现ClassCastException。总的来说,对集合的存储类型不指定是一种不太好的行为,为此才出现了泛型。如:

  1. @org.junit.Test
  2. public void t6() {
  3. List list = new ArrayList();
  4. //可以添加各种类型的对象
  5. list.add("666");
  6. list.add(new Phone());
  7. //索引为1的元素类型并不是String
  8. String s = (String) list.get(1);
  9. }

image.png
jdk1.5之前,容器的操作除了元素的类型不确定之外,其它的部分都是确定的,如:如何添加这个元素、如果删除这个元素。这个时候把元素的类型设计成一个参数,而这个类型参数就是泛型了。
如:指定List集合只能操作String类型的对象:
image.png

2.2 泛型的定义和目的

定义

允许在定义类、接口时通过一个标识来标识类中的某个属性的类型或者是某个方法的返回值及参数类型,而这个参数类型在使用时(如:继承该类或实现该接口,用这个类型声明变量、创建对象)确定。泛型的一种较为准确的说法就是为了参数化类型,或者说可以将类型当作参数传递给一个类或者是方法

目的

Java泛型是一种语法糖,在编译阶段完成类型的转换的工作,避免在运行时强制类型转换而出现ClassCastException(类型转化异常)。使用泛型主要的优点就是能够在编译期而不是运行时检测出类型相关的异常。
下面是泛型的优点:

  • 与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
  • 当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
  • 泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为 Cache这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。

image.pngimage.png

3、泛型结构

3.1 泛型类、泛型接口

泛型类和接口在jdk中是大量使用的,这里以List接口为例:
image.png
image.png
由于List接口是一个泛型接口,那么其下的toArray方法中的T的类型必须和List的类型一模一样,我们只需要在实例化List时指定T到底是哪个类型即可。其中的T只能是一个类,不可以使用基本数据类型来填充,但可以使用其包装类代替。如:
image.png

注意事项

  • 一个泛型类或接口可以指定多个泛型参数,如: ```java public class Student{

}

  1. - 泛型类的构造器写法:“pulic Student(){ }”,而不是“pulic Student<E, T, P>(){ }”
  2. - 泛型不同的引用不能相互赋值,如:List<String>和List<Integer>时无法相互赋值
  3. - 泛型如果不知道具体类型,泛型对应的类型均按Object处理
  4. - 异常类时不可以是泛型的
  5. - 在接口/类上声明了泛型,该类中的静态方法是不可以使用这个泛型的,原因是:类的泛型是在类实例化的时候指定的,但是类中的静态方法确实在类实例化化之前就被编译了
  6. - 创建泛型数组时,不能使用“E[] arr=new E[size]”,但是可以使用“E[] arr=(E[])new Object[size]”
  7. - 泛型类是可以被其它类继承的,通常有以下四种情况:
  8. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/12564419/1618494905732-4d7dd769-d01e-4252-aa0e-d0c216e92377.png#crop=0&crop=0&crop=1&crop=1&height=408&id=czAvL&margin=%5Bobject%20Object%5D&name=image.png&originHeight=816&originWidth=1760&originalType=binary&ratio=1&rotation=0&showTitle=false&size=623944&status=done&style=none&title=&width=880)<br />其中第四中相当于“class Child extends Father<Object , Object>”
  9. - 如果AB的父类,G<A>不是G<B>的父类,但A<G>是B<G>的父类
  10. <a name="wCs4y"></a>
  11. ### 3.2 泛型方法
  12. 方法也可以被泛型化,简称为泛型方法,泛型方法可以存在泛型类,也可以存在非泛型类,并且存在泛型类时,泛型方法中的泛型和类中的泛型没有任何关系。泛型方法定义的结构通常是:<br />“[访问权限] <T> 返回类型 方法名(参数,也可以是泛型指定) 抛出的异常”,如:
  13. ```java
  14. public class Student {
  15. public <E> List<E> fromArrToList(E[] arr) {
  16. List<E> list = new ArrayList<>();
  17. Collections.addAll(list, arr);
  18. return list;
  19. }
  20. public static void main(String[] args) {
  21. Student student = new Student();
  22. String[] stringArr = new String[]{"aaa", "bbb", "ccc"};
  23. List<String> list = student.fromArrToList(stringArr);
  24. System.out.println(list);
  25. }
  26. }

image.png
注意:泛型类和接口中的泛型是不能被静态方法使用的,但是这里的泛型方法确是可以指定为泛型的,原因是:泛型方法的泛型是在被调用的时候确定的,不管是实例调用还是静态调用都没有区别,因此静态调用并不影响泛型方法中泛型的指定,所以Java允许泛型方法是静态的。如:

  1. public class Student {
  2. public static <E> List<E> fromArrToList(E[] arr) {
  3. List<E> list = new ArrayList<>();
  4. Collections.addAll(list, arr);
  5. return list;
  6. }
  7. public static void main(String[] args) {
  8. Student student = new Student();
  9. String[] stringArr = new String[]{"aaa", "bbb", "ccc"};
  10. List<String> list = Student.fromArrToList(stringArr);
  11. System.out.println(list);
  12. }
  13. }

泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,泛型方法始终以自己定义的类型参数为准。为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名,如:

  1. public class Test1<T>{
  2. public void testMethod(T t){
  3. System.out.println(t.getClass().getName());
  4. }
  5. public <E> E testMethod1(E e){
  6. return e;
  7. }
  8. }

4、通配符和限定符

4.1 通配符

泛型允许使用通配符“?”,通配符“?”是所有泛型的共同父类,它表示任何泛型,因此List<?>
是List、List这类的父类。在读取List<?>类型的集合时,都是安全并且可以读取到的,不管list的真实类型是什么,它包含的都是Object;但是如果想把一个对象写入List<?>则是不行的,因为并不知道List<?>的真实类型,所以不允许写入对象(null除外)。

注意事项
通配符“?”不能声明在类上:
image.png
通配符不能用在泛型方法声明上,返回值类型不能使用“<?>”:
image.png
通配符“?”不能用于创建对象上,一般用于对象的引用上:
image.png
image.png

4.2 限定符

在java泛型中,? 表示通配符,代表未知类型,< ? extends Object>表示上边界限定通配符,< ? super Object>表示下边界限定通配符。
注意通配符 与 T 的区别:
T:作用于模板上,用于将数据类型进行参数化,不能用于实例化对象。
?:在实例化对象的时候,不确定泛型参数的具体类型时,可以使用通配符进行对象定义。

上界限定符

<? extends A>是一个上界通配符,它的意思是限定“?”的类型必须是A类的子类或者A类本身,如果A类是一个接口,那么“?”必须是A类的一个实现类。
比如:<? extend Number> ,表示所有继承Number的子类,但是具体是哪个子类,无法确定,所以调用add的时候,要add什么类型,谁也不知道,故add受阻。但是get的时候,不管是什么子类,不管追溯多少辈,肯定有个父类是Number,所以,我都可以用以最大的父类Number来get,也就是把所有的子类向上转型为Number。如:

  1. List<? extends Number> eList = null;
  2. eList = new ArrayList<Integer>();
  3. //语句1,正确:取出Number(或者Number子类)对象直接赋值给Number类型的变量是符合java规范的。
  4. Number numObject = eList.get(0);
  5. //语句2,错误:取出Number(或者Number子类)对象直接赋值给Integer类型(Number子类)的变量是不符合java规范的。
  6. Integer intObject = eList.get(0);
  7. //语句3,错误:List<? extends Number> eList并没有确定实例化对象的具体类型,因此无法add具体对象至列表。
  8. eList.add(new Integer(1));

上界类型通配符add方法受限,但可以获取列表中的各种类型的数据,并赋值给父类型(extends Number)的引用。因此如果想从一个数据类型里获取数据,就使用 ? extends 通配符。

下界限定符

<? super B>是一个下界通配符,它的意思是限定“?”的类型必须是B类的父类或者B类本身。
比如:<? super Integer>,表示Integer的所有父类,包括Integer,一直可以追溯到老祖宗Object 。那么当我add的时候,不能add Integer的父类,因为List<? super Integer>并没有确定里面存放的到底是哪个父类。但是可以add Integer及其子类。因为不管子类是什么类型,它都可以向上转型为Integer及其所有的父类甚至转型为Object 。但是当get的时候,Integer的父类这么多,除了Object,其他的都不行。如:

  1. List<? super Integer> sList = null;
  2. sList = new ArrayList<Number>();
  3. //语句1,错误:List<? super Integer> 无法确定sList中存放的对象的具体类型,因此sList.get获取的值存在不确定性,子类对象的引用无法赋值给兄弟类的引用,父类对象的引用无法赋值给子类的引用,因此语句错误。
  4. Number numObj = sList.get(0);
  5. //语句2,错误:同上理由
  6. Integer intObj = sList.get(0);
  7. //语句3,正确:所以类都可以转化为老祖宗Object
  8. Object object = sList.get(0);
  9. //语句4,正确:子类对象的引用可以赋值给父类对象的引用,因此语句正确
  10. sList.add(new Integer(1));

下界类型通配符get方法受限,但是可以add下界的子类或其本身。因此如果想把一个对象写进一个数据结构,就使用 ? super 通配符。

总结

  • 限定通配符总是包括自己
  • 上界类型通配符:add方法受限
  • 下界类型通配符:get方法受限
  • 如果想从一个数据类型里获取数据,使用 ? extends 通配符
  • 如果想把对象写入一个数据结构里,使用 ? super 通配符
  • 如果既想存,又想取,那就别用限定符
  • 不能同时声明泛型通配符上界和下界