一、泛型
泛型是JavaSE1.5的新特性,泛型的本质是参数化类型,也就是说操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类,泛型接口,泛型方法。
Java语言引入泛型的最大好处就是安全简单,可以将运行时类型相关的错误提前到编译时错误。
在没有泛型之前,通过对类型Object的引用来实现参数的任意化,这种方式带来的缺点就是需要使用显示的强制类型转换,而这种转换是要求开发者对实际参数可以预知的情况下进行的。对于强制类型转换来说,编译时是不会报错的,而是在运行时才会出现异常,所以存在安全隐患。
泛型的好处就是在编译时就会检查类型安全,并且所有的强制类型转换都是自动和隐式的,提高了代码的重用率。
通常情况下,编译器会有两种处理泛型的方式:
1.Codespecialization
在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码or二进制代码)。例如,针对一个泛型list,可能需要针对string,integer,float产生三份目标代码
2.Codesharing
对每个泛型类只生成唯一的一份目标代码;该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换
Java编译器通过Codesharing的方式为每个泛型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这唯一的字节码表示上。将多种泛型类型实例都映射到唯一的字节码表示是通过类型擦除实现的。
类型擦除:指的是通过类型参数合并,将泛型类型实例关联到同一份字节码文件上。编译器只为泛型类型生成一份字节码文件,并将实例关联到这份字节码文件上。类型擦除的关键在于从泛型类型中清除类型参数相关的信息,并且在必要时添加类型检查和类型转换的方法。
类型擦除可以简单理解为,将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码文件。类型擦除的主要步骤:
将所有的泛型参数用其最左边界(最顶级的父类类型)类型替换
移出所有的类型参数
第一个泛型类Comparable<A>擦除后A被替换为最左边界Object。Comparable<NumericValue>的类型参数NumericValue被擦除掉,但是这直接导致NumericValue没有实现接口Comparable的compareTo(Objectthat)方法,于是编译器充当好人,添加了一个桥接方法
第二个示例中限定了类型参数的边界<AextendsComparable<A>>A,A必须为Comparable<A>的子类,按照类型擦除的过程,先讲所有的类型参数ti换为最左边界Comparable<A>,然后去掉参数类型A,得到最终的擦除后结果
所有的泛型类的泛型参数在编译时都会被擦除,虚拟机运行时没有泛型,只有普通类和普通方法。
Java泛型不支持基本数据类型,即ArrayList<int>是不被允许的
在泛型代码内部,无法获得任何有关泛型参数类型的信息,如果传入的类型参数,为T,那么在泛型代码内部你不知道T有什么方法,属性,关于T的一切信息都丢失了。
创建泛型的时候需要指明类型,让编译器尽早的做参数检查。
忽略编译器的警告,那意味着有潜在的ClassCastException
Java的泛型类型不能用于new构建对象,(也不能用于初始化数组)
定义泛型方法的规则:
1.类型参数声明部分在方法返回值类型之前。
2.每一个类型参数声明部分包含一个或多个参数类型,参数见用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
3.泛型方法体的声明和其他方法一样,注意参数类型只能代表引用数据类型,不能是基本数据类型。
4.类型参数能作为方法的返回值类型,并且能作为泛型方法得到的实际参数类型的占位符
二、泛型类
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开
三、泛型接口
数组的协变:
上例中main方法中的第一行,创建了一个 Apple 数组并把它赋给 Fruit 数组的引用。这是有意义的,Apple 是 Fruit 的子类,一个 Apple 对象也是一种 Fruit 对象,所以一个 Apple 数组也是一种 Fruit 的数组。
尽管Apple[]可以“向上造型”为Fruit[],但数组中元素的实际类型还是Apple,我们只能向数组中添加Apple或者Apple的子类。虽然Fruit数组中可以加入Orange数组,编译器也不会报错,但是,在运行时就会出现异常,因为JVM知道数组的实际类型是Apple[]。
上面代码无法编译,尽管Apple是Fruit的子类型,但是ArrayList<Apple>不是ArrayList<Fruit>的子类型,泛型不支持协变。
上述例子中,List的类型是List<? extends Fruit>,我们可以把他当做:一个类型的List,这个类型可以使继承了Fruit的某种类型。他表示:“某种特定的类型,但List没有指定”。例如:list引用可以指向某个类型的List,只要这个类型继承自Fruit,可以使Fruit或者Apple,比如例子中的new ArrayList<Apple>,但是为了向上转型给list,list并不关心这个具体类型是什么。
Fruit是它的上边界,而且我们并不知道这个List到底持有什么类型,所以除了Null之外的其他类型都是不安全的。所以如果做了泛型的向上转型,(List<? extends Fruit> flist = new ArrayList<Apple>()),我们也就失去了对这个list添加任何对象的能力。
如果调用某个方法返回Fruit的方法,这是安全的,因为在list中,不管它实际的类型到底是什么,都肯定能转型为Fruit,所以编译器允许返回Fruit。
上面的例子中,flist 的类型是List<? extends Fruit>,泛型参数使用了受限制的通配符,所以我们失去了向其中加入任何类型对象的例子,最后一行代码无法编译
但是 flist 却可以调用 contains 和 indexOf 方法,它们都接受了一个 Apple 对象做参数。如果查看 ArrayList 的源代码,可以发现 add() 接受一个泛型类型作为参数,但是 contains 和 indexOf 接受一个 Object 类型的参数,所以如果我们指定泛型参数为 <? extends Fruit> 时,add() 方法的参数变为 ? extends Fruit,编译器无法判断这个参数接受的到底是 Fruit 的哪种类型,所以它不会接受任何类型,然而,contains 和 indexOf 的类型是 Object,并没有涉及到通配符,所以编译器允许调用这两个方法。这意味着一切取决于泛型类的编写者来决定那些调用是 “安全” 的,并且用 Object 作为这些安全方法的参数。如果某些方法不允许类型参数是通配符时的调用,这些方法的参数应该用类型参数,比如 add(E e)
还有一种通配符是无边界通配符,它的使用形式是一个单独的问号:List<?>,也就是没有任何限定
- List<?> list 表示 list 是持有某种特定类型的 List,但是不知道具体是哪种类型。那么我们可以向其中添加对象吗?当然不可以,因为并不知道实际是哪种类型,所以不能添加任何类型,这是不安全的。而单独的 List list ,也就是没有传入泛型参数,表示这个 list 持有的元素的类型是 Object,因此可以添加任何类型的对象,只不过编译器会有警告信息
四、反射
- Java反射机制允许程序在运行时通过Reflection APIs取得任何一个已知名称的class的内部信息,包括其修饰符、父类、接口、构造方法、属性、方法等,并可于运行时改变属性值或者调用方法等;
- Java反射机制是Java语言的一个重要特性,使得Java语言具备“动态性”: JAVA反射机制是构建框架技术的基础所在,例如后续学习的Spring框架等,都使用到反射技术;
- 在运行时获取任意一个对象所属的类的相关信息;
- 在运行时构造任意一个类的对象;
- 在运行时获取任意一个类所具有的成员变量和方法;
- 在运行时调用任意一个对象的方法;
- Java的反射机制依靠反射API实现,反射API主要包括以下几个类,后续学习: java.lang.Class类是反射机制中最重要的类,是使用反射机制时的“起点”;
- java.lang.Class类:代表一个类;
- java.lang.reflect.Field 类:类的成员变量(成员变量也称为类的属性);
- java.lang.reflect.Method类:类的方法;
- java.lang.reflect.Constructor 类:类的构造方法;
- java.lang.reflect.Array类:动态创建数组,以及访问数组的元素的静态方法;
- JVM运行程序时,会将要使用到的类加载到内存中,同时就会自行为这个类创建一个Class对象,这个对象中就封装了类的所有信息,包括类中的属性、方法、构造方法、修饰符等;
- Java.lang.Class类中定义了一系列的getXXX方法,可以获取Class中封装的其他信息;
获取Class类对象:
1 package com.chinasofti.reflect; 2 3 public class newClass { 4 public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { 5 6 // 利用反射创建类(class)对象的三种方式: 7 // 1. 8 Class<Student> studentClass = Student.class; 9 10 // 2. 11 Class<?> aClass = Class.forName("com.chinasofti.reflect.Student"); 12 13 // 3. 14 Student s = new Student(); 15 Class<? extends Student> aClass1 = s.getClass(); 16 17 // 对比一下三种方式创建的对象是否一致 18 System.out.println(studentClass==aClass && aClass==aClass1); 19 20 // 使用class对象的newInstance方法创建对象 21 Student student = studentClass.newInstance(); 22 Student student1 = (Student) aClass.newInstance(); 23 Student student2 = aClass1.newInstance(); 24 } 25 }
获取构造方法对象:
1 package com.chinasofti.reflect; 2 3 import java.lang.reflect.Constructor; 4 import java.lang.reflect.InvocationTargetException; 5 6 public class newConstructor { 7 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 8 // 先获取class对象 9 Class<Student> studentClass = Student.class; 10 // 获取class对象的无参构造器对象 11 Constructor<Student> constructor = studentClass.getConstructor(); 12 // 使用无参构造器的对象来创建对象 13 Student student = constructor.newInstance(); 14 System.out.println(student); 15 16 System.out.println("--------------------"); 17 // 通过指定参数获得指定有参构造器的对象 18 Constructor<Student> constructor1 = studentClass.getConstructor(String.class, int.class); 19 // 创建有参对象 这里的参数是一个Object类型的可变参数 20 // Student s = constructor1.newInstance(new Object[]{"张三", 231}); 21 Student student1 = constructor1.newInstance("张三", 123); 22 System.out.println(student1); 23 } 24 }
获取方法:
1 package com.chinasofti.reflect; 2 3 import java.lang.reflect.InvocationTargetException; 4 import java.lang.reflect.Method; 5 6 public class getMethods { 7 public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { 8 Class<Student> studentClass = Student.class; 9 // 通过class对象获取指定名称的方法对象 10 Method getName = studentClass.getMethod("getName"); 11 // 使用方法对象中的invoke方法来调用方法 invoke(Object obj, Object... args) 第一个参数是对象 第二个参数是可变参数,用于传参 12 Object invoke = getName.invoke(studentClass); 13 System.out.println(invoke); 14 } 15 }
一个简易的api方法:
1 package com.chinasofti.reflect; 2 3 import java.lang.reflect.Constructor; 4 import java.lang.reflect.InvocationTargetException; 5 import java.lang.reflect.Method; 6 7 public class UtilMethod { 8 public static void main(String[] args) { 9 Object getName = start("com.chinasofti.reflect.Student", "getName"); 10 System.out.println(getName); 11 } 12 13 14 /** 15 * 反射调用方法 返回方法调用后的返回值 16 * @param className 类名 17 * @param methodName 方法名 18 * @param arr 方法参数列表 19 * @return 方法的返回值 20 */ 21 public static Object start(String className, String methodName, Object ... arr) { 22 // 声明一个Object类型的返回值 23 Object res = null; 24 try { 25 // 创建一个class对象 26 Class<?> aClass = Class.forName(className); 27 // 获取class对象的无参构造方法 28 Constructor<?> constructor = aClass.getConstructor(); 29 // 使用无参构造方法来创建对象 30 Object o = constructor.newInstance(); 31 // 通过class对象获取全部方法 32 Method[] methods = aClass.getMethods(); 33 // 开始遍历 寻找符合参数中的方法名称的方法 34 for (Method method:methods) { 35 if(method.getName().equals(methodName)){ 36 // 找到该方法 然后使用invoke调用 获取参数 37 res = method.invoke(o,arr); 38 break; 39 } 40 } 41 } catch (ClassNotFoundException e) { 42 e.printStackTrace(); 43 return null; 44 } catch (InstantiationException e) { 45 e.printStackTrace(); 46 return null; 47 } catch (InvocationTargetException e) { 48 e.printStackTrace(); 49 return null; 50 } catch (NoSuchMethodException e) { 51 e.printStackTrace(); 52 return null; 53 } catch (IllegalAccessException e) { 54 e.printStackTrace(); 55 return null; 56 } 57 // 返回调用方法后的返回值 58 return res; 59 } 60 }
五、内省
在实际编程中,我们常常需要一些用来包装值对象的类,例如Student、Employee、Order。这样类中往往没有业务方法,只是为了把需要处理的实体对象进行封装,有这样的特征:
1.属性都是私有的;
2.有无参的public构造方法;
3.对私有属性根据需要提供公有的getXxx方法以及setXxx方法;例如属性名称为name,则有getName方法返回属性name值,setName方法设置name值;注意方法的名称通常是get或set加上属性名称,并把属性名称的首字母大写;这些方法称为getters/setters;getters必须有返回值没有方法参数;setter值没有返回值,有方法参数;
符合这些类特征的类,被称为JavaBean;
内省机制就是基于反射的基础,Java语言对Bean类属性、事件的一种缺省处理方法;
与Java内省有关的主要类及接口有:
1.java.beans.Introspector类:为获得JavaBean属性、事件、方法提供了标准方法;通常使用其中的getBeanInfo方法返回BeanInfo对象。
2.java.beans.BeanInfo接口:不能直接实例化,通常通过Introspector类返回该类型对象,提供了返回属性特征描述符对象(PropertyDescriptor)、方法描述符对象(MethodDescriptor)、bean描述符(BeanDescriptor)对象的方法;
3.java.beans.PropertyDescriptor类:用来描述一个属性,该属性有getter及setter方法。
- 只要类中有getXXX方法,或者setXXX方法,或者同时有getXXX及setXXX方法,其中getXXX方法没有方法参数,有返回值;setXXX方法没有返回值,有一个方法参数;那么内省机制就认为XXX为一个属性;
1 package com.chinasofti.reflect; 2 3 import org.apache.catalina.util.Introspection; 4 5 import java.beans.*; 6 7 // 内省 当一个getXXX方法没有参数 有返回值 或者一个setXXX方法有参数 没有返回值 那么内省机制就认为该XXX是属性 8 public class IntrospectionTest { 9 public static void main(String[] args) throws IntrospectionException { 10 // 通过内省对象获取beaninfo对象(使用Student的class对象反射获取) 11 BeanInfo beanInfo = Introspector.getBeanInfo(Student.class); 12 // 通过beaninfo对象 获取所有属性存储到数组中 13 PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); 14 // 遍历输出内省机制所认为的属性名 15 for (PropertyDescriptor p:propertyDescriptors) { 16 System.out.println(p.getName()); 17 } 18 19 // 获得类class对象 20 BeanDescriptor beanDescriptor = beanInfo.getBeanDescriptor(); 21 System.out.println("----" + beanDescriptor.getName()); 22 // 获取该类的方法集合 23 MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors(); 24 for (MethodDescriptor m:methodDescriptors) { 25 System.out.println(m.getName()); 26 } 27 } 28 }
- 很多框架都使用了内省机制检索对象的属性,定义属性名字时,名字最好起码以两个小写字母开头,例如stuName,而不要使用sName,某些情况下,可能会导致检索属性失败;
- 再次强调,内省机制检索属性时,是根据getter和setter方法确认属性名字,而不是根据类里声明的属性名决定;