20145312 《Java程序设计》第十周学习总结
学习笔记
Chapter 17反射与类加载器
17.1 运用反射
.class文档反应了类基本信息,因而从Class等API取得类信息的方式就称为反射。
17.1.1 Class与.class文档
- java.lang.Class的实例代表Java应用程序运行时加载的.class文档。
- 可以通过Object的getClass()方法,或者是通过.class常量取得每个对象接口。
- 在取得Class对象后,就可以操作Class对象的公开方法,取得基本信息。
代码如下:
package Reflection;
import static java.lang.System.out;
public class ClassInfo {
public static void main(String[] args){
Class clz=String.class;
out.println("类名称:"+clz.getName());
out.println("是否为接口:"+clz.isInterface());
out.println("是否为基本类型:"+clz.isPrimitive());
out.println("是否为数组对象:"+clz.isArray());
out.println("父类名称:"+clz.getSuperclass());
}
}
结果如下:
类名称:java.lang.String
是否为接口:false
是否为基本类型:false
是否为数组对象:false
父类名称:class java.lang.Object
- 使用类声明参考名称并不会加载.class文档,可设计测试类来验证。
- Some类定义了static区块,默认首次加载.class文档时会执行静态区块。通过文本模式下显示类型,了解何时加载.class文档.
代码如下:
package Reflection;
import static java.lang.System.out;
public class SomeDemo {
public static void main(String[] args){
Some s;
out.println("声明Some参考名称");
s=new Some();
out.println("生成Some实例");
}
}
结果如下:
声明Some参考名称
载入Some.class文档
生成Some实例
17.1.2 使用Class.forName()
使用Class.forName()方法实现动态加载类,可用字符串指定类名称来获得类相关信息。
代码如下:
package Reflection;
import static java.lang.System.out;
public class InfoAbout {
public static void main(String[] args){
try{
Class clz=Class.forName(args[0]);
out.println("类名称:"+clz.getName());
out.println("是否为接口:"+clz.isInterface());
out.println("是否为基本类型:"+clz.isPrimitive());
out.println("是否为数组对象:"+clz.isArray());
out.println("父类名称:"+clz.getSuperclass());
}catch (ArrayIndexOutOfBoundsException e){
out.println("没有指定类名称");
}catch (ClassNotFoundException e){
out.println("找不到指定的类"+args[0]);
}
}
}
或者可以使用forName()第二个版本,将initialize设定为false,这样会在建立类实例时执行static区块。
代码如下:
package Reflection;
import static java.lang.System.out;
class Some2{
static {
System.out.println("[执行静态区块]");
}
}
public class SomeDemo2 {
public static void main(String[] args) throws ClassNotFoundException{
Class clz=Class.forName("Reflection.Some2",false,SomeDemo2.class.getClassLoader());
out.println("已载入Some2.class");
Some s;
out.println("声明Some参考名称");
s=new Some();
out.println("生成Some实例");
}
}
结果如下:
已载入Some2.class
声明Some参考名称
[执行静态区块]
生成Some实例
17.1.3 从Class获得信息
取得Class对象后,就可以取得.class文档中记载的信息:
包对应类型是:java.lang.Package
构造函数对应类型是:java.lang.reflect.Constructor
方法成员对应类型是:java.lang.reflect.Method
数据成员对应类型是:java.lang.reflect.Field
例如取得指定String类的包名称:
Package p=String.class.getPackage();
System.out.println(p.getName()); //显示java.lang
17.1.4 从Class建立对象
如果不知道类名称,就利用Class.forName()动态加载.class文档,取得Class对象后,利用newInstance()方法建立实例,如:
Class clz=Class.forName(args[0]);
Object obj=clz.newInstance();
想采用影片链接库来播放动画,利用接口定义出影片链接库该有的功能。
代码如下:
package Reflection;
import java.util.Scanner;
public class MediaMaster {
public static void main(String[] args) throws ClassNotFoundException,InstantiationException,IllegalAccessException{
String playerImp1=System.getProperty("Reflection.playerImp1");
Player player=(Player) Class.forName(playerImp1).newInstance();
System.out.println("输入想播放的影片:");
player.play(new Scanner(System.in).nextLine());
}
}
package Reflection;
public class ConsolePlayer implements Player {
@Override
public void play(String video){
System.out.println("正在播放"+video);
}
}
指定-DReflection.PlayerImp1=Reflection.ConsolePlayer。
执行结果如下:
输入想播放的影片:Harry Potter
正在播放:Harry Potter
17.1.5 操作对象方法与成员
- 使用 invoke()方法来动态调用指定的方法。
代码如下:
package Reflection;
public class Student {
private String name;
private Integer score;
public Student(){}
public Student(String name,Integer score){
this.name=name;
this.score=score;
}
public void setName(String name){
this.name=name;
}
public String getName(){
return name;
}
public void setScore(Integer score){
this.score=score;
}
public Integer getScore(){
return score;
}
}
17.1.6 动态代理
静态代理
在静态代理实现中,代理对象与被代理对象必须实现统一接口,在代理对象中可以实现日志服务。如下可定义一个Hello接口:
package Reflection;
public interface Hello {
void hello(String name);
}
如果有个HelloSpeaker类操作了Hello接口:
package Reflection;
public class HelloSpeaker implements Hello{
public void hello(String name){
System.out.printf("哈啰,%s%n",name);
}
}
在HelloSpeaker类中没有日志程序代码,日志程序代码会放在代理对象中,代理对象同样也要实现Hello接口。
代码如下:
package Reflection;
import java.util.logging.*;
import java.util.logging.Logger;
public class HelloProxy implements Hello {
private Hello helloObj;
public HelloProxy(Hello helloObj){
this.helloObj=helloObj;
}
public void hello(String name){
log("hello方法开始...");//日志服务
helloObj.hello(name); //执行商业规则
log("hello方法结束...");//日志服务
}
private void log(String msg){
// Logger logger=Logger.getLogger(HelloProxy.class.getName()).log(Level.INFO,msg);
}
}
静态代理必须为个别接口操作个别代理类,多的代理接口定义多个代理对象,操作和维护代理对象会有不少的负担。
动态代理
- 反射API中提供动态代理相关类,该机制可使一个处理者代理多个接口的操作对象。
- 处理者类必须操作java.lang.reflect.InvocationHandler接口。
代码如下:
package Reflection;
import java.lang.reflect.*;
import java.util.logging.*;
public class LoggingHandler implements InvocationHandler {
private Object target;
private Object bind(Object target){
this.target=target;
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
Object result=null;
try{
log(String.format("%s()呼叫开始...",method.getName()));
result=method.invoke(target,args);
log(String.format("%s()呼叫结束...",method.getName()));
}catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e){
log(e.toString());
}
return result;
}
private void log(String message){
Logger.getLogger(LoggingHandler.class.getName()).log(Level.INFO,message);
}
}
接下来可使用LoggingHandler的blind()方法来绑定被代理对象:
package Reflection;
public class ProxyDemo {
public static void main(String[] args){
LoggingHandler loggingHandler=new LoggingHandler();
Hello helloProxy=(Hello) loggingHandler.blind(new HelloSpeaker());
helloProxy.hello("Justin");
}
}
17.2了解类加载器
17.2.1 类加载器级层架构
- 加载器的分类及作用:
Bootstrap Loader:产生Exrtended Loader和System Loader
Exrtended Loader:父加载器为Bootstrap Loader
System Loader:父加载器为Exrtended Loader
-
Bootstrap Loader
通常由C撰写而成。若是Oracle的JDK,Bootstrap Loader会搜索系统参数sun.boot.class.path中指定位置的类,默认是JRE目录的classes中的.class文档,或lib目录中.jar文档里的类。 -
Exrtended Loader
由Java撰写而成,会搜索系统参数java.ext.dirs中指定位置的类,默认是JRE目录libextclasses中的.class文档。 -
System Loader
由Java撰写而成,会搜索系统参数java.class.path中指定位置的类,也就是CLASSPATH路径,默认是当前工作路径下的.class文档。 -
加载类是会以
Bootstrap Loader->Exrtended Loader->System Loader
的顺序寻找类,如果所有类加载器都找不到指定类,就会抛出java.lang.NoClassDefFoundError
。 -
对null调用getParent()方法会抛出
NullPointedException
异常。 -
ClassLoader
可以使用loadClass()方法加载类,使用localClass方法加载类时,默认不会执行静态区块,真正使用类建立实例时才会执行静态区块。 -
关系代码如下:
package Reflection;
import static java.lang.System.out;
public class ClassLoaderHierarchy {
public static void main(String[] args){
Some some=new Some();
Class clz=some.getClass();
ClassLoader loader=clz.getClassLoader();
out.println(loader);
out.println(loader.getParent());
out.println(loader.getParent().getParent());
}
}
结果如下:
载入Some.class文档
sun.misc.Launcher$AppClassLoader@1b84c92
sun.misc.Launcher$ExtClassLoader@140e19d
null
17.2.2 建立ClassLoader实例
- 使用
URLClassLoader
来产生新的类加载器,需要java.net.URL作为其参数来指定类加载的搜索路径。使用URLClassLoader的loadClass()
方法加载指定类时,会先委托父加载器代为搜索。 - 由同一类加载器载入的.class文档,只会有一个Class实例。如果同一.class文档由两个不同的类加载器载入,则会有两份不同的Class实例。
代码如下:
package Reflection;
import static java.lang.System.out;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassLoaderDemo {
public static void main(String[] args){
try {
String path=args[0];
String clzName=args[1];
Class clz1=loadClassFrom(path,clzName);
out.println(clz1);
Class clz2=loadClassFrom(path,clzName);
out.println(clz2);
out.printf("clz1与clz2为%s实例",clz1==clz2 ? "相同":"不同");
}catch (ArrayIndexOutOfBoundsException e){
out.println("没有指定类加载路径与名称");
}catch (MalformedURLException e){
out.println("加载路径错误");
}catch (ClassNotFoundException e){
out.println("找不到指定的类");
}
}
private static Class loadClassFrom(String path,String clzName)throws ClassNotFoundException,MalformedURLException{
ClassLoader loader=new URLClassLoader(new URL[] {new URL(path)});
return loader.loadClass(clzName);
}
}
path可以输入不在System Loader以上层级类加载器搜索路径的其他路径。
Chapter 18 自定义泛型、枚举与注释
18.1 自定义泛型
泛型定义:
仅定义在方法上的泛型语法
用来限制泛型可用类型的extends与super关键字
?类型通配字符的使用
18.1.1 使用extends与?
- 若extends之后指定了类与接口,想再指定其他接口,可以使用&连接。
代码如下:
package Generics;
import java.util.Arrays;
public class Sort {
public static <T extends Comparable<T>> T[] sorted(T[] array){
T[] arr=Arrays.copyOf(array,array.length);
sort(arr,0,arr.length-1);
return arr;
}
private static void sort(Object[] array,int left,int right){
if(left<right){
int q=partition(array,left,right);
sort(array,left,q-1);
sort(array,q+1,right);
}
}
private static int partition(Object[] array,int left,int right){
int i=left-1;
for(int j=left;j<right;j++){
if(((Comparable) array[j]).compareTo(array[right])<=0){
i++;
swap(array,i,j);
}
}
swap(array,i+1,right);
return i+1;
}
private static void swap(Object[] array,int i,int j){
Object t=array[i];
array[i]=array[j];
array[j]=t;
}
}
package Generics;
public class SortDemo {
public static void main(String[] args){
String[] strs={"3","2","5","1"};
for(String s:Sort.sorted(strs)){
System.out.println(s);
}
}
}
结果如下:
1
2
3
5
- 如果B是A的子类,而Node< B>可视为一种Node< A>,则称Node具有共变性或有弹性的。Java泛型不具有共变性,可以使用类型通配字符?与extends来声明变量,使其达到类似的共变性。
- 若声明?不搭配extends,则默认为? extends Object。
Node<?> node = null;//相当于Node<? extends Object>
- java的泛型语法在执行时期实际上只会知道是Object类型,由于无法在执行时期获得类型信息,编译程序只能就编译时期看到的类型来做检查,因而造成以上谈及的限制。
18.1.2 使用super与?
- 如果B是A的子类,而Node< A>可视为一种Node< B>,则称为Node具有逆变性。Java泛型不具有逆变性,可以使用类型通配字符?与super来声明,使其达到类似的逆变性的效果。
- 若泛型类或接口不具共变性或逆变性,则称为不可变的或严谨的。
18.2 自定义枚举
18.2.1 了解java.lang.Enum类
- 直接撰写程序继承Enum类会被编译程序拒绝。
- Enum是个抽象类,无法直接实例化,它操作了Comparable接口。Action的构造函数被声明为private,因此只能在Action类中调用。
- 在JDK1.4之前撰写的API,仍是使用interface定义常数作为枚举值。
- Enum的equals()方法与hashCode()方法基本上继承了Object的行为,但被标示为final。由于标示为final,所以定义枚举是,不能重新操作equals()与hashCode(),这是因为枚举成员,在JVM中智慧存在单一实例,Object定义的equals()与hashCode()作为对象相等性比较是适当的定义。
18.2.2 enum高级运用
- values()方法,将内部维护Action枚举实例的数据复制后返回。如果想要知道有哪些枚举成员,就可以使用这个方法。
- Enum类可以自行定义构造函数,但不得为公开构造函数,也不可以在构造函数中调用super()。
- 在enum中调用构造函数比较特别,直接在枚举成员后加上括号,就可以指定构造函数需要的自变量。
- 在static区块中,编译程序仍自行维护name与ordinal的值,接着才是调用自定义构造函数时传入的value值。
- 特定值类本体语法:在枚举成员后,直接加上{}操作Command的execute()方法,这代表着每个枚举实例都会有不同的execute()曹组欧,在职责分配上,比switch的方式清楚许多。特定值类本体语法不仅在操作接口时可以使用,,也可以运用在重新定义父类方法。
18.3 关于注释
18.3.1 常用标准注释
@Override
就是标准注释,被注释的方法必须是父类或接口中已定义的方法,请编译程序协助是否真的为重新定义方法。@Deprecated
如果某个方法原先存在与API中,后来不建议再使用,可以在该方法上注释。若有用户后续想调用或重新定义这个方法,编译程序会提出警告。对于支持泛型的API,建议明确指定泛型真正类型,如果没有指定,编译程序会提出警告。@SuppressWarnings
指定抑制unchecked的警告产生:@SuppressWarnings(value={"unchecked"})
@SafeVarargs
表明开发人员确定避免了heap pollution问题。heap pollution问题就是编译程序无法检查执行时期的类型错误,无法具体确认自变量类型。@FunctionalInterface
让编译程序可协助检查interface是否可做为lambda的目标类型
18.3.2 自定义注释类型
- 标示注释:就是注释名称本身就是信息,对编译程序或应用程序来说,主要是检查是否有注释出现,并作出对应的动作。
2.相关规则:
(1)如果注释名称本身无法提供足够信息,设置单值注释
(2)注释属性也可以用数组形式指定。
(3)在定义注释属性时,如果属性名称为value,则可以省略属性名称,直接指定值。
(4)对成员设定默认值,使用default关键字即可。
(5)要设定数组默认值,可以在default之后加上{},必要时{}中可放置元素值。
- 定义注释时,可使用
java.lang.annotation.Target
限定时可指定java.lang.annotation.ElementType
的枚举值。 - 在制作JavaDoc文件时,默认不会将注释数据加入文件中,如果想要将注释数据加入文件,可以使用
java.lang.annotation.Documented
。 - 默认父类设定的注释,不会被继承至子类,在定义注释时,设定java.lang.annotation.Inherited注释,就可以让注释被子类继承。
18.3.3 JDK8标注增强功能
ElementType
的枚举成员是用来限定哪个声明位置可以进行标注。在JDK8中,增加了两个枚举成员TYPE _PARAMETER
、TYPE _USE
。ElementType.TYPE _ USE
可用于标注在各式类型,一个标注如果被设定为ElementType.TYPE_USE
,只要是类型名称,都可以进行标注。@Repeatable
可以让你在同一个位置重复相同标注@Filters
作为收集重复标注信息的容器,而每个@Filters储存各自指定的字符串值。
18.3.4 执行时期读取注释信息
- 自定义注释,默认会将注释信息存储于.class文档,可被编译程序或位码分析工具读取,但执行时期无法读取注释信息,在执行时期读取注释信息,可以使用
java.lang.annotation.Retention
搭配java.lang.annotation.RetentionPolicy
枚举指定。 - RetentionPolicy为RUNTIME的时机,在于让注释在执行时期提供应用程序信息,可使用
java.lang.reflect.AnnotatedElement
接口操作对象取得注释信息。 - JDK 8中新增了
getDeclaredAnnotation()
、getDeclaredAnnotationsByType()
、getAnnotationsByType()
三个方法。
getDeclaredAnnotation()
可以让你取回指定的标注,在指定@Repeatable
的标注时,会寻找收集重复标注的容器。
getDeclaredAnnotationsByType()
、getAnnotationsByType()
就不会处理@Repeatable
的标记。