------- android培训、java培训、期待与您交流! ----------
1. Eclipse及IDE开发工具介绍
Eclipse和MyEclipse的关系:MyEclipse其实是Eclipse的插件扩展,就是说Eclipse假设其他插件和变成MyEclipse,MyEclipse是用来开发JavaEE的。后来软件开发商家Eclipse和这些插件集合在一起,就形成了现在的MyEclipse。
Eclipse是IDE开发工具,IDE是integrated developmentenvironment,表示集成开发环境。2. Eclipse工程管理与快捷键配置
IDE开发工具都是使用工程化的关联方式来管理一个项目的开发过程,一般来说一个相对独立的项目就是一个工程,一个项目涉及多个java文件,资源文件等用一个工程来管理。如果不适用工程管理,需要逐一编译这些源文件,维护起来很麻烦。
一个workspace中包含多个project,一个workspace保留了Eclipse的一套环境选项配置。例如,所使用的javac和java命令,等等,详情请看window->preferences。如果要为Eclipse配置一套环境选项,可以再创建一个workspace。Package explorer视图窗口的filters菜单项,可以显示空的父包(次功能默认是关闭的)。在一个workspace中配置的环境选项,会对该workspace下的所有工程起作用。
如果新建新的工程想要放在另一个workspace,那么可以新建一个workspace:File->SwitchWorkspace->Other,新建成功后,Eclipse重新启动,原来的workspace关闭。原来配置的快捷键在新的workspace可能会不起作用,这时候需要重新配置快捷键,例如Alt+/就是补全代码的快捷键,但是在新的workspace中无法起作用,这时候重新配置的步骤是:
Window->Preferences->General->Keys,在“type filter text”中输入content assist,因为内容补全是属于内容帮助。如果content assist的快捷键不是自己想要的,可以解除绑定,设置成自己希望的快捷键,如下图所示:注意:有时,会出现两个冲突的快捷键,即存在两个功能不同但是快捷键相同的。这时候,这两个快捷键均不起作用,可以解除绑定其中一个快捷键,另外的快捷键就可以使用了。
3. Eclipse视图管理与程序调试
在程序开发过程中,可以对程序进行调试,通过调试可以找到程序有问题的地方。调试之前需要给程序设置断点:起始断点和终止断点。
1. 在需要设置断点的地方双击即可设置断点。
2. 在代码编辑窗口中,右击空白处选中Debug AS->JavaApplication,之后Eclipse弹出却换到Debug透视图,单击“Yes”跳转到Debug透视图:
在Debug透视图中有许多小窗口组成,如果想要查看变量值,选中并右击相应的变量,选择“Watch”,在右上方即可出现变量值窗口。按住F6即可让程序单步执行,按住F5即可让程序执行完毕,或者点击下面的按钮:
4. 配置Eclipse编译与运行环境
如果将workspace的编译器版本更改,运行某个project可能会出现版本号问题“Bad Version number”,这是因为workspace的编译环境和project不一致。例如workspace的JDK版本为1.7,而project的JDK版本为1.6,就有可能出现问题。如何查看某个项目运行时使用的编译器版本呢?
右击该项目->Run As->Run Configuration:
高版本的java能运行低版本的javac编译的程序,第版本的java不能运行高版本的javac编译的程序。所以出现“Bad Version number”问题,适用于workspace的编译器版本高于运行工程的编译器版本,这时候需要降低workspace编译器的版本,操作步骤为:
window->Preferences->Java->Compiler:
如果不想更改workspace的JDK版本,而又想让project使用高版本的JDK,这时候需要导入高版本的JDK:
window->Preferences->Java->InstalledJREs:
在Eclipse的workspace中所有的工程都继承workspace的配置,其中某个工程也可以覆盖workspace的配置,这其实也是java面向对象的体现。
5. Eclipse中配置java模板代码
当我们在Eclipse的比较器窗口中输入syso,然后按住Alt+/,然后就出现代码:
System.out.println();
当我们选择某块代码,在空白处右键->Surroundwith->Try/catch Block,然后就出现代码:
try {
i = 1;
i++;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
这是因为在Eclipse中有一个模板,如果我们想自定义try{ }finally代码块模板,该如何自定义呢?
新建模板:在Eclipse中依次打开:Window->Preferences->Java->Editor—>Templates,如果需要新建模板,点击右边的“New”按钮
在创建模板对话框中新建try { } finally{ }代码块的模板,内容如下:
在Eclipse中输入模板名称tryf,按住Alt+/就可以快速生成try-finally代码块。或者选中需要try-finally的代码块,右击选择Surround with->tryf(),即可生成下面的模板:
try {
} finally {
}
6. 在Eclipse中导入已有工程
将项目导入到Eclipse中的workspace步骤是:File->Import->Genearl->ExitingProject into Workspace:
如果导入进来的Project没有JRE System Library,这时候需要给项目增加JRE System Library,操作步骤为:
右击项目->Properties->Java BuildPath->Libraries->Add Library->Java Sytem Library->Workspace defaultJRE:
为了便于管理JAR文件,我们也可以新建User Library,该User Library就可以用来保存用户的jar文件,操作流程:
右击项目->Properties->JavaBuild Path->Libraries->Add Library->User Library->next->UserLibraries->New,输入User Library name:7. 静态导入与编译器语法设置
import可以导入一个类或者某个包中所有类,import static语句导入一个类中的某个静态方法或者所有静态方法。
例子:求两个数中的最大值;求两个数相减结果的绝对值。在Math类下面有一些方法,可以实现常用的数学运算,现在我们就使用Math类下面的方法实现这个需求://求两个数的最大值
int max = Math.max(6, 8);
//求两个数相减的绝对值
int abso = Math.abs(6-8);
发现,求最大值和绝对值的方法都是Math类下面的静态方法,要想使用该静态方法,就需要前缀类名,这样的方式有时候很麻烦。如果想要使用某个类的静态方法,又不想前缀类名,这时候就需要使用使用静态导入,静态导入是JDK1.5提供的新特性:
静态导入语法:import static,如下:
//静态导入 Math 类下面的所有静态方法
import static java.lang.Math.*;
public class StaticImport {
public static void main(String[] args) {
// 求两个数的最大值
int max = max(6, 8);
// 求两个数相减的绝对值
int abso = abs(6 - 8);
}
}
8. 可变参数与OverLoad相关面试题分析
可变参数:一个方法接收的参数个数不固定,例如:
add(3, 5, 8);
add(2, 4);
可变参数的特点:
1. 如果有多个参数,可变参数只能出现在参数列表的最后;
2. 位于变量类型和变量名之前,前后有无空格都可以;
3. 调用可变参数的方法时,编译器为该可变参数隐含创建一个数组,在方法体中以数组的形式访问可变参数。
可变参数是JDK1.5成新特性,在JDK1.5之前,可变参数需要方法重载来实现,方法重载不仅需要书写大量的代码,而可变参数只需要一个方法即可实现。
可变参数的定义语法:public void add(int val, int ... args) { }
可变参数例子:
public class VariableParameter {
//可变参数方法
public static int add(int val, int ... args) {
int sum = val;
for(int i = 0; i < args.length; i++) {
sum += args[i];
}
return sum;
}
public static void main(String[] args) {
//调用可变参数
System.out.println(add(2, 4));
System.out.println(add(2, 4, 6));
System.out.println(add(2, 4, 6, 8));
}
}
overload(重载)和override(重写)的区别:
9. 增强for循环
增强for循环语法:
for(type 变量名:集合变量名) { ... }
集合可以是数组或者实现了Iterable接口的集合类。例子:
public static int sum(int[] arr) {
int value = 0;
//增强for循环
for(int arg : arr) {
value += arg;
}
return value;
}
10. 基本数据类型的自动拆装箱与享元设计模式
基本数据类型的自动装箱与拆箱是JDK1.5提供的新特性。
自动装箱:将一个基本数据类型自动地变成其对应的数据类型的对象。
自动拆箱:将对象变成其对应的基本数据类型。
例子: //自动装箱,将一个基本数据类型变成对应数据类型的对象
Integer i = 8;
//自动拆箱:i是Integer对象,不支持加法运算,
//进行加法运算之前先将其变成基本数据类型,这就叫拆箱
int value = i + 12;
System.out.println(value);
重要知识点:Integer对象的值,如果在一个字节内(-128~127),当创建对象时,会将一个字节内的对象值缓冲到一个缓存池中,下次如果再次创建Integer对象并且其值和上一次创建的相同,就直接从缓存池中取得该值,这样可以节省内存空间。因为这些小的整数使用频率很高,会反复使用,所以再次使用时直接从缓存池中取就可以了,而不需要给这些对象分配新的内存空间。这是一种设计模式——享元设计模式(flyweight)。
在word文字编辑中就是用到享元设计模式,在word中输入英文字母,有26个英文字母,如果每次输入一个英文字母就创建一个对象,这样会出现创建成千上万个对象的问题,浪费内存空间,使用享元设计模式只需要创建26个对象对应26个英文字母。
对于值在一个字节内(-128~127)内是Integer对象,如果它们的值相同则它们时同一个对象,因为指向同一块内存空间,例子: Integer i1 = 127;
Integer i2 = 127;
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i1 == i2);//true
System.out.println(i3 == i4);//false
Integer类中有一个静态方法,可以将一个整数变成Integer对象,但是这不是自动装箱。如果两个Integer对象将一个字节内的相同值的整数变成Integer对象,那么它们是同一个对象,这和上来例子相同。该方法还可以将字符串变成Intege对象:
//整数变成Integer对象
Integer i1 = Integer.valueOf(8);
Integer i2 = Integer.valueOf(8);
//字符串变成Integer对象
Integer i3 = Integer.valueOf("123");
Integer i4 = Integer.valueOf("123");
System.out.println(i1 == i2);//true
System.out.println(i3 == i4);//true
11. 枚举的作用介绍
问题:为什么要有枚举?要定义星期几或性别的变量,该怎定义?假设用1-7分别表示星期一到星期日,但是有人可能写成intweekday = 0;
枚举就是要让某个类型的变量的取值只能为若干个固定值中的一个,否则编译器会报错。枚举可以让编译器在编译时就可以控制源程序中填写的非法值,普通变量的方式在开发阶段无法实现这样目标。
12. 用普通类模拟枚举的实现原理
下面例子通过普通类实现枚举,来说明枚举的原理。例子中每一天都是WeekDay的一个对象:
class WeekDay {
//星期一到星期日,为了代码简便这里只定义两天,星期日定义为每一个星期的第一天
public final static WeekDay SUN = new WeekDay();
public final static WeekDay MON = new WeekDay();
//私有构造方法
private WeekDay() {}
//后一天
public WeekDay nextDay() {
if(this == SUN)
return MON;
else
return SUN;
}
//覆盖toString方法
public String toString() {
return this == SUN ? "SUM" : "MON";
}
}
public class EnumDemo {
public static void main(String[] args) {
WeekDay day = WeekDay.SUN;
System.out.println(day); //SUN
System.out.println(day.nextDay()); //MON
}
}
我们发现,如果使用上了例子的方法,如果有星期一到星期天,nextDay方法里就需要写大量的if-else语句。采用抽象方法定义nextDay方法就将大量的if-else语句转移成每一个独立的类。这样,如果有7天,就可以将原来的7个if-else语句变成7个子类里的nextDay方法具体实现nextDay,例子如下(在上述代码基础上修改),不过该例子代码还是过多,下一章我们将讲解枚举类,枚举类更加便捷实现该功能:
abstract class WeekDay {
//星期一到星期日,为了代码简便这里只定义两天,星期日定义为每一个星期的第一天
public final static WeekDay SUN = new WeekDay() {
//使用匿名内部类来创建对象,下同
public WeekDay nextDay() {
return MON;
}
};
//匿名内部类创建对象
public final static WeekDay MON = new WeekDay() {
public WeekDay nextDay() {
return SUN;
}
};
//私有构造方法
private WeekDay() {}
public abstract WeekDay nextDay();
//覆盖toString方法
public String toString() {
return this == SUN ? "SUM" : "MON";
}
}
public class EnumDemo {
public static void main(String[] args) {
WeekDay day = WeekDay.SUN;
System.out.println(day); //SUN
System.out.println(day.nextDay()); //MON
}
}
13. 枚举的基本应用
定义一个WeekDay枚举类,枚举类是一个特殊的类,其中每个元素都是该类的一个实例对象。枚举类为我们自动实现了toString方法,用户不需要手动创建toString方法,而且枚举类中还提供了许多有用的方法:
public class EnumDemo2 {
//枚举类
public enum WeekDay {
SUN, MON, TUE, WED, THU, FIR,SAT;
}
public static void main(String[] args) {
//创建枚举类对象
WeekDay day = WeekDay.FIR;
System.out.println(day); //FRI
//对象名称
System.out.println(day.name()); //FRI
//对象所在位置,序号
System.out.println(day.ordinal());//5
//下面是枚举类的静态方法
System.out.println(WeekDay.SAT);//SAT
//字符串转换成对象
System.out.println(WeekDay.valueOf("SUN"));//SUN
//values方法将枚举类对象变成数组
WeekDay[] days = WeekDay.values();
for(WeekDay d : days) {
System.out.println(d);
}
}
}
14. 带有构造方法的枚举
在上面一章中,枚举类没有构造方法,下面的例子就为枚举类定义构造方法,枚举类的构造方法必须是私有的。枚举什么时候调用构造方法呢?当初始化枚举中的每一个成员变量时,调用枚举的构造方法,而且默认调用空参数的构造方发:
public class EnumDemo2 {
//枚举类
public enum WeekDay {
SUN, MON, TUE, WED, THU, FIR,SAT;
//无参构造方法
private WeekDay() {
System.out.println("Frist");
}
//重载构造方法
private WeekDay(int day) {
System.out.println("Second");
}
}
public static void main(String[] args) {
WeekDay day = WeekDay.SUN;
}
}
输出结果(因为枚举中有7个变量,所以有7次调用构造方法):
枚举默认调用无参数构造方法,那么如何让其调用有参数的构造方法呢?方法就是在枚举变量的后面加上括号,在括号里写上实参。如果括号中没有实参,则调用的是无参数构造方法,如下:
public class EnumDemo2 {
//枚举类
public enum WeekDay {
SUN(1), MON(), TUE(3), WED, THU, FIR,SAT;
//无参构造方法
private WeekDay() {
System.out.println("Frist");
}
//重载构造方法
private WeekDay(int day) {
System.out.println("Second");
}
}
public static void main(String[] args) {
WeekDay day = WeekDay.SUN;
}
}
输出结果:
15. 实现带有构抽象方法的枚举
下面定义一个交通灯类,该类是一个枚举类,交通灯有三个枚举实例对象:RED、GREEN和YELLOW:
//交通灯枚举类
enum TrafficLamp {
//红灯、绿灯、黄灯,后面出现{}是因为这些成员由其子类来实现
RED{
//匿名内部类实现父类的抽象方法,下同
public TrafficLamp nextLamp() {
return GREEN;
}
},
GREEN{
public TrafficLamp nextLamp() {
return YELLOW;
}
},
YELLOW{
public TrafficLamp nextLamp() {
return RED;
}
};
//抽象方法:下一盏灯
public abstract TrafficLamp nextLamp();
}
在还没有运行该程序之前,打开该程序所在的workspace,发现有3个匿名内部类:
下面扩展上例功能,给交通灯枚举类添加亮灯的持续时间方法:
//交通灯枚举类
enum TrafficLamp {
//红灯、绿灯、黄灯,后面出现{}是因为这些成员由其子类来实现
//红灯持续时间30秒
RED(30){
//实现父类的抽象方法,下同
public TrafficLamp nextLamp() {
return GREEN;
}
},
GREEN(45){
public TrafficLamp nextLamp() {
return YELLOW;
}
},
YELLOW(5){
public TrafficLamp nextLamp() {
return RED;
}
};
//抽象方法:下一盏灯
public abstract TrafficLamp nextLamp();
//时间:每一盏灯持续的时间
private int time;
//构造方法
private TrafficLamp(int time) {
this.time = time;
}
}
如果枚举只有一个成员时,就可以使用单例设计模式来实现。也可以这么说,要创建单例,可以使用枚举。
16. 分析反射的基础_Class
反射的基石:反射是java1.2之后出现的新特性,不是JDK1.5的新特性。Java程序中的各个Java类属于同一类事物,描述这类事物的Java类名就是Class,例如Person类代表人,它的实例对象张三、李四是一个个具体的人,Class类名代表Java类,那它的实例对象有分别对应什么呢?Class类是反射的基石。
Java类是用于描述一类事物的共性,该类事物有什么属性,没有什么属性,至于这个属性是什么,则由该类的实例对象来确定,不同的实例对象有不同的属性值。Java程序中的各个Java类属于同一类事物,所以我们可以使用Class(不是小写的class)关键字来描述这类事物。Class类描述的信息有:类的名字、类发访问属性、类所属的包名、字段名的列表、方法名称的列表,等等。学习反射首先就要了解Class这个类。
什么是字节码:假设有Person这个类,当我们在源程序中使用到Person这个类时,首先会将该类的二进制代码编译成.class文件存储在硬盘上,创建Person对象时需要把这些硬盘上的二进制代码加载到内存中,也就是说首先把这个类的字节码加载到内存中,再使用这个类的字节码复制出一个个Person对象来。当程序中使用到Person、Math和Data这三个类,那么内存中就有三分字节码。Person字节码就是Class的实例对象,Data类的字节码也是Class类的实例对象。每一个字节码就是一个Class的实例对象,如下例子:
Person p1 = new Person("Steve", 23);
Class c1 = Person.class;
System.out.println(c1);
//输出:class com.itheima.day25.Person
如何获取到各个字节码的实例对象(Class类型):
1. 类名.class,例如System.class;
2. 对象.getClass,例如new Data().getClass()。该方法是通过类的对象获取字节码实例对象,对象是通过字节码创建的,所有可以通过对象获取到该字节码的实例对象。
3. 使用静态方法Class.forName(“类名”),例如Class.forName(“java.util.Data”)。该方法说明,该类还没有加载进来,这时候可以使用Class发forName方法获取字节码。这种方式说明Java虚拟机中还没有加载该类,所有先把该类加载进来:
九个预定义Class实例对象:
8个基本数据类型对应8个Class实例对象,还有一个void类型对应一个Class实例对象。
//九个预定义Class实例对象
Class c3 = byte.class;
Class c1 = int.class;
Class c4 = long.class;
Class c2 = char.class;
Class c6 = float.class;
Class c5 = double.class;
Class c7 = void.class;
Class c8 = boolean.class;
判断是否为同一份字节码:
//判断是不是同一分字节码
System.out.println(c1 == c2);//true
System.out.println(c2 == c3);//true
System.out.println(c1 == c3);//true
System.out.println(int.class == Integer.class);//false
//Integer.TYPE表示包装类对应的基本数据类型的字节码
System.out.println(int.class == Integer.TYPE);//true
判断是否为基本数据类型的字节码:
//判读是否为基本数据类型的字节码
Class c1 = String.class;
System.out.println(c1.isPrimitive()); //false
System.out.println(int.class.isPrimitive()); //true
System.out.println(Integer.class.isPrimitive());//false
//数组不是基本数据类型
System.out.println(int[].class.isPrimitive()); //false
//判断是不是数组:数组Class的实例对象,使用的方法是isArray
System.out.println(int[].class.isArray()); //true
总之,在源程序中出现的类型,都有各自的Class实例对象,例如int、int[]、void等。
17. 理解反射概念
反射就是把Java类中的各种成分映射成相应的Java类。例如,一个Java类中用一个Class类的对象来表示,一个类中的组成成分:成员变量、方法、构造方法、包等信息也用一个个Java类来表示,就像汽车是一个类,发动机、变速箱等等也是一个个类。表示Java类的Class类显然提供一系列方法,来获取其中的变量、方法、构造方法、修饰符、包等信息,这些信息就是用相应的实例对象来表示,它们是Field、Method、Constructor、Package等等。
一个类中的每个成员都可以用相应的反射API的一个实例对象来表示,通过调用Class类的方法得到这些实例对象后,怎么使用这些实例对象呢?这正是学习和应用反射的要点。
不过反射会导致程序性能下降。18. 构造方法的反射应用
Constructor类代表某个类的一个构造方法,得到某个类的所有构造方法,如下:
Constructor[] constructors = Class.forName("java.lang.String").getConstructors();
得到某一个类的构造方法如下:
Constructor con = Class.forName("java.lang.String").getConstructor(StringBuilder.class);
使用构造方法创建实例对象:
//通常方式:
String str1 = new String(new StringBuffer("abc"));
//反射方式:
String str2 = (String) constructor.newInstance(new StringBuffer("abc"));
反射创建实例对象的方式是:先由class获取到构造方法Constructor,再通该构造方法创建实例对象,例子:
String str1 = new String(new StringBuffer("abc"));
//下面使用反射方式实现上一行代码
//获取构造方法
Constructor constructor = String.class.getConstructor(StringBuffer.class);
//构造方法创建实例对象:不能写成(String) constructor.newInstance("abc"),类型不匹配
String str2 = (String) constructor.newInstance(new StringBuffer("abc"));
//获取字符串的第3个字符
System.out.println(str2.charAt(2));
Class.newInstance()方法:该方法内部先得到默认的构造方法,然后用构造方法创建实例对象。该方法内部的具体代码是怎样的呢?用到了缓存机制来保存默认构造方法的实例对象。
例子:
Object obj = Class.forName("java.lang.String").newInstance();
19. 成员变量的反射
Field类:Field类代表某个类中的一个成员变量。
问题:得到的Field对象是对应到类上的成员变量,还是对应到对象上的成员变量?类只有一个而该类的实例对象有多个,如果是与对象关联,哪关联的是哪个对象呢?所以自动fieldX代表的是x的定义,而不是具体的x变量,例如:
public class ReflectPoint {
private int x;
public int y;
private ReflectPoint(int x, int y) {
this.x = x;
this.y = y;
}
public static void main(String[] args) throws Exception {
ReflectPoint pt = new ReflectPoint(3, 5);
//通过字节码获取成员变量 y,fieldY不代表一个具体的值,只表示一个变量
//fieldY不是对象的变量,而是类的变量
Field fieldY = pt.getClass().getField("y");
//获取私有变量的方式要使用.getDeclaredField
Field filedX = pt.getClass().getDeclaredField("x");
//获取变量fieldY在对象pt上的值,私有的成员变量无法获取到
System.out.println(fieldY.get(pt));//5
//获取私有变量
// filedX.setAccessible(true);//教学视频中需要加上该行代码,下面方法才可以使用,因为该成员变量是私有的,需要设置为使用
System.out.println(filedX.get(pt));//3
}
}
20. 成员变量反射综合案例
练习:将任意一个对象中的所有String类型的变量锁对应的字符串内容中的“b”改成“a”。
class Str {
public int value = 124;
public String str1 = "ball";
public String str2 = "basketball";
public String str3 = "itcast";
public String toString() {
return str1+":"+str2+":"+str3;
}
}
public class FieldReflectTest {
//修改指定字符
private static void changeStringValue(Object obj) throws Exception {
//得到所有的成员变量
Field[] fields = obj.getClass().getFields();
for(Field field : fields) {
//如果变量为String类型,字节码使用等号相比较,一般不用equals,因为麻烦
if(field.getType() == String.class) {
//获取成员变量
String oldValue = (String) field.get(obj);
//将oldValue的'b'字符替换成'a'字符
String newValue = oldValue.replace('b', 'a');
//将对象设置成新的值
field.set(obj, newValue);
}
}
}
public static void main(String[] args) throws Exception {
Str str = new Str();
changeStringValue(str);
System.out.println(str);
}
}
输出结果:
aall:aasketaall:itcast
21. 成员方法的反射
Method类:Method类代表类中的一个成员方法,得到类中的某一个方法:
Method charAt = Class.forName("java.lang.String").getMethod("charAt", int.class);
调用方法:
//通常方式:
System.out.println(str.charAt(1));
//反射方式:
System.out.println(charAt.invoke(str, 1));
如果传递给Method对象的invoke()方法的一个参数为null,这有着什么样的意义呢?说明该Method对象对应的是一个静态方法。如下面例子。
methodCharAt是方法Method的类的对象,invoke表示让该对象调用方法,参数str1表示该方法作用在哪个对象上,如果str1写成null,表示对象为空,那么说过该方法为静态方法:
String str1 = "abc";
Method methodCharAt = String.class.getMethod("charAt", int.class);
//invoke表示让方法执行调用动作
System.out.println(methodCharAt.invoke(str1, 1));//输出:b
JDK1.4和JDK1.5的区别:
按照JDK1.4的语法,需要将一个数组作为参数传递给invoke方法时,数组中的每个元素分别对应被调用方法中的一个参数,所以调用方法charAt的代码也可以用JDK1.4改写成为:charAt.invoke(“Str”, new Object[]{1})形式:
String str1 = "abc";
Method methodCharAt = String.class.getMethod("charAt", int.class);
System.out.println(methodCharAt.invoke(str1, new Object[] {2}));//输出:c
22. 对接收数组参数的成员方法进行反射
需求:写一个程序,这个程序能够根据用户提供的类名,去执行该类中的米main方法。
问题:启动Java程序的main方法是一个字符串数组,即:
public static void main(String[] args),
通过反射来调用该main方法,如何为invoke方法传递参数呢?按照JDK1.5的语法,整个数组是一个参数,按照JDK1.4的语法,数组中的每一个元素对应一个参数。当把一个字符串数组作为参数传递给invoke方法时,javac到底按照哪种语法进行处理呢?JDK1.5要兼容JDK1.4,会按照JDK1.4语法进行处理,即把数组打散成为单独的参数。所以,在main传递参数时,不能使用代码:
mainMethod.invoke(null, new String[] {xxx}),
javac只把它当做JDK1.4来进行理解,而不把它当做JDK1.5的语法来理解,因此会出现参数类型不对的问题。
解决办法:
mainMethod.invoke(null, new Ojbect[] {newString[]{xxx}});
mainMethod.invoke(null, (Object)new String[]{xxx}),编译器会做特殊处理,编译器不会把参数当做数组来看待,也就将数组打散成若干个参数了。
为什么要使用反射的方式调用main方法,我们先通过普通方式调用main方法,就能够理解原因了。下面这个例子是使用普通方法调用main方法:
public class ArgumentDemo {
public static void main(String[] args) throws Exception {
//调用TestArgument的main方法
TestArgument.main(new String[] {"111", "222", "333"});
}
}
class TestArgument {
public static void main(String[] args) {
for(String arg : args) {
System.out.println(arg);
}
}
}
运行 ArgumentDemo ,输出结果:
111
222
333现在通过反射实现上面的功能:
public class ArgumentDemo {
public static void main(String[] args) throws Exception {
String className = args[0];
Method mainMethod = Class.forName(className).getMethod("main", String[].class);
mainMethod.invoke(null, new String[]{"111", "222", "333"});
}
}
class TestArgument {
public static void main(String[] args) {
for(String arg : args) {
System.out.println(arg);
}
}
}
在Eclipse中右击TestArgument类,选择“Copy Qualified Name”,然后在Eclipse空白处右击,选择Run As->Run Configurations,选择运行的主函数:
在Arguments中配置参数,该参数是通过TestArgument方法的main方法传递进去的:
单击“Run”后,报错,如下,表示参数个数错误。这是因为JDK1.5为了兼容JDK1.4语法的原因(详情请看本章前面的问题部分):
wrong number of arguments现在修改上面的代码,将三个字符串组成的数组包装成一个Object数组中的一个元素(只显示修改部分):
mainMethod.invoke(null, new Object[] {new String[]{"111", "222", "333"}});
运行
ArgumentDemo
类,输出结果和上面的普通方式的例子相同。
还有另一种不需要包装成Object对象的方式,也能实现该功能,如下(只显示修改部分的代码),该方法是将数组强转成Object对象:
mainMethod.invoke(null, (Object)new String[]{"111", "222", "333"});
23. 数组与Object的关系及其反射类型
数组的反射:
1. 具有相同维度和元素类型的数组属于同一个类型,即具有相同的Class实例对象。
2. 代表数组的Class的实例对象的getClass方法,返回的父类为Object类对应的Class。
3. 基本类型的数组可以被当做Object类型使用,不能当做Object[]类型使用。非基本类型的一维数组,即可以当做Object类型使用,也可以当做做Object[]类型使用。
public class ArrayReflect {
public static void main(String[] args) {
int[] arr1 = new int[3];
int[] arr2 = new int[4];
int[][] arr3 = new int[2][3];
String[] str = new String[3];
//同一个类型的数组,并且都是一维数组,属于同一份字节码
System.out.println(arr1.getClass() == arr2.getClass());//true
//下面的编译不通过
//不是同一份字节码,因为arr3是二维数组,arr1是一维数组,两者维度不同
// System.out.println(arr1.getClass() == arr3.getClass());
//不是同一份字节码,因为数组类型不同
// System.out.println(arr1.getClass() == str.getClass());
}
}
下面例子分析2、3中的内容(上面提到数组反射的2、3):
int[] arr1 = new int[3];
int[] arr2 = new int[4];
int[][] arr3 = new int[2][3];
String[] str = new String[3];
//获得arr1的父类名称,下面两行输出结果都是:java.lang.Object
System.out.println(arr1.getClass().getSuperclass().getName());
System.out.println(str.getClass().getSuperclass().getName());
Object obj1 = arr1;
Object obj2 = arr2;
Object obj3 = str;
//基本类型的一维数组不能转换成Object类型的数组,编译错误
// Object[] obj4 = arr1;
//此时,obj5相当于arr3的第二维数组
Object[] obj5 = arr3;
Object[] obj6 = str;
Arrays.asList()方法处理int[]和Object[]时有差异:
Array类中有许多操作数组的方法,如果我们想打印数组,通过syso(arr1)是不行的,打印的结果是数组的哈希值。这时候可以使用Array类中的方法,打印数组arr1:
int[] arr1 = new int[]{1, 2, 3};
String[] str = new String[]{"a", "b", "c"};
//输出结果:[[I@12402e11]
System.out.println(Arrays.asList(arr1));
//Sting对象转换成为List,输出结果:[a, b, c]
System.out.println(Arrays.asList(str));
问题:为什么使用Arrays.asList(arr1)将数组转换成List,后打印的结果和Sting类型数组转换成List打印的结果不同呢?因为方法Arrays.asList()接收的参数是Object类型的,String类中的数组可以转换成Object类型的数组,但是int类型的数组不能转换成为Object类型的数组,asList方法会将int类型的数组作为一个参数来处理。详细内容请看本章最前面的内容的3部分。
24. 数组的反射应用
数组的反射:
1. 具有相同维度和元素类型的数组属于同一个类型,即具有相同的Class实例对象。
2. 代表数组的Class的实例对象的getClass方法,返回的父类为Object类对应的Class。
3. 基本类型的数组可以被当做Object类型使用,不能当做Object[]类型使用。非基本类型的一维数组,即可以当做Object类型使用,也可以当做做Object[]类型使用。
4. Arrays.asList()方法处理int[]和Object[]时有差异(上一节有代码示例)。
5. Array工具类(不是Arrays类)用于完成对数组的反射操作。
需求:定义一个打印Object的方法printObject,如果传入的参数的一个元素,则直接打印出来。如果传入的参数是数组,就把数组中的元素一个个打印出来。下面的例子演示Array工具类(java.util.Arrays)的使用,通过该工具类获取到数组中的元素:
public class ArrayReflect {
public static void main(String[] args) {
int[] arr1 = new int[]{1, 2, 3};
String[] str = new String[]{"a", "b", "c"};
printObject(arr1);
printObject(str);
printObject("xyz");
}
//打印Object的方法
public static void printObject(Object obj) {
Class cla = obj.getClass();
//如果是数组
if(cla.isArray()) {
//获取数组长度
int len = Array.getLength(obj);
for(int i = 0; i < len; i++) {
//获取数组的第i个元素
System.out.println(Array.get(obj, i));
}
}
else {//不是数组,直接打印
System.out.println(obj);
}
}
}
25. ArrayList和HashSet的比较及Hashcode分析
25.1 ArrayList
ArrayList集合的特点:
例子:
class ReflectPoint {
private int x;
public int y;
public ReflectPoint(int x, int y) {
this.x = x;
this.y = y;
}
}
public class ReflectTest {
public static void main(String[] args) {
ReflectPoint pt1 = new ReflectPoint(3, 3);
ReflectPoint pt2 = new ReflectPoint(5, 5);
ReflectPoint pt3 = new ReflectPoint(3, 3);
Collection list = new ArrayList();
list.add(pt1);
list.add(pt1);//相同元素
list.add(pt2);
list.add(pt3);
//集合大小:4
System.out.println(list.size());
}
}
25.2 HashSet
HashSet集合的特点:
在上面例子中(25.1),如果将ArrayList改为HashSet,那么只有三个元素。如果我们希望p1和p3被认为是同一个元素,这是需要覆盖Hashcode、和equals方法。
自动生成equals方法和hashCode方法:
右击空白处,选择Source->GeneratehashCode() and equals():
class ReflectPoint {
private int x;
public int y;
public ReflectPoint(int x, int y) {
this.x = x;
this.y = y;
}
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ReflectPoint other = (ReflectPoint) obj;
if (x != other.x)
return false;
if (y != other.y)
return false;
return true;
}
}
public class ReflectTest {
public static void main(String[] args) {
ReflectPoint pt1 = new ReflectPoint(3, 3);
ReflectPoint pt2 = new ReflectPoint(5, 5);
ReflectPoint pt3 = new ReflectPoint(3, 3);
Collection list = new HashSet();
list.add(pt1);
list.add(pt1);
list.add(pt2);
list.add(pt3);
//集合大小:2,pt1和pt3是同一个元素
System.out.println(list.size());
}
}
25.3 HashCode
我们知道,Set集合中不能存放相同元素,那么它是如何实现元素的唯一性的呢?加入向Set集合中存入一万个元素,那么是不是一个个比较这些元素是不是相等呢?如果这样比较,那效率就很低了。
为了解决这个问题,就发明了HashCode(哈希值)。这种方式将集合分成若干个存储区域,每一个对象可以计算出一个哈希值,可以将哈希值分组,每组分别对应某个存储区域,根据一个对象的哈希值就可以确定该对象存储在哪个区域,如下图所示:HashSet就是采用哈希算法,实现元素存储的集合,它内部使用对某个数值n进行取余的方式对哈希码进行分组和划分对象存储区域。Object类中定义了一个hashCode()方法来返回每个Java对象的哈希值,当从HashSet集合中查找某个对象时,Java系统首先调用对象的hashCode方法获取对象的哈希码,根据该哈希码找到相应的存储区域,然后取出该存储区域的每个元素与该对象进行equals方法比较,这样不用变量集合中的所有元素就可以得到结论。
可见,hashCode具有很好的对象检索性。如果元素不是存储到集合中,就没有必要使用hashcode来比较了。
注意:
当一个对象存储到 HashSet集合(不是其他集合)中,就不能修改这个对象中那些参与计算哈希值的某些字段了。否则,对象修改后的哈希值与最初存储进HashSet集合中的哈希值就不同了。这种情况下,即使在contains方法使用该对象的当初引用作为参数去HashSet集合中检索,也将返回找不到对象的结果。这也会导致无法从HashSet集合中单独删除当前对象,从而造成内存泄露。下面例子演示(ReflectPoint类代码在25.2节中): ReflectPoint pt1 = new ReflectPoint(3, 3);
ReflectPoint pt2 = new ReflectPoint(5, 5);
ReflectPoint pt3 = new ReflectPoint(3, 3);
Collection hashSet = new HashSet();
hashSet.add(pt1);
hashSet.add(pt1);
hashSet.add(pt2);
hashSet.add(pt3);
pt1.y = 7;
//移除对象pt1,并没有移除成功,因为修改了参与hashCode计算的字段 y
hashSet.remove(pt1);
//集合大小:2
System.out.println(hashSet.size());
26. 框架的概念及用反射技术开发框架的原理
反射的作用:实现框架功能。下面我们完成一个小框架,来说明反射的作用。
例子:我做房子给用户,由用户自己安装门窗和空调,我做的房子就是框架,用户需要使用我的框架,把门窗安装到我的框架中。框架与工具类有区别,工具类被用户的类调用,而框架则是调用用户提供的类。
框架要解决的核心问题:
1. 我在做框架(房子)时,用户可能还在上小学,还不会写程序呢,我写的框架怎样才能调用都用户以后写的类(门窗)呢?
2. 因为在写程序是无法知道要调用的类名,所有在出现中无法直接new某个对象的实例对象,而要使用反射方式来完成。
首先在新建一个配置文件:File->New->File,命名为config.properties
在该config.properties文件中输入一下内容:
className=java.util.ArrayList
代码如下(不显示ReflectPoint类的代码,ReflectPoint类在25..2节中有):
public class ReflectTest {
public static void main(String[] args) throws Exception {
InputStream in = new FileInputStream("config.properties");
//Properties可以将保存在硬盘文件上的键值对读取进来,还可以将键值对保存到硬盘文件
Properties pro = new Properties();
//加载
pro.load(in);
in.close();
//获取键对应的值
String className = pro.getProperty("className");
//创建实例对象
Collection collection = (Collection) Class.forName(className).newInstance();
ReflectPoint pt1 = new ReflectPoint(3, 3);
ReflectPoint pt2 = new ReflectPoint(5, 5);
ReflectPoint pt3 = new ReflectPoint(3, 3);
collection.add(pt1);
collection.add(pt1);
collection.add(pt2);
collection.add(pt3);
//集合大小:4
System.out.println(collection.size());
}
}
27. 用类加载器的方式管理资源和配置文件
问题:在上面的例子中,加载了配置文件config.properties,加载该文件使用的是相对路径,在实际开发中,是不能使用相对路径的。因为该配置文件保存在项目根目录下,当我开发完项目后,并不是把整个项目提供给使用者,而是将项目的bin目录下的class。在class文件中本不存在配置文件。但是使用绝对路径,也会出现问题,假设配置文件config.properties保存在D盘下,当回用户并没有D盘这个盘符,这就造成程序运行问题。
那么该如何管理配置文件呢?可以使用完整路径,但是这个完整路径不是硬编码,而是运算处理的。
当我们使用.class文件时,内加载器就会将该类文件加载到内存中(字节码加载到内内存中)。既然内加载器有这样的功能,我们就可以使用内加载器加载配置文件。内加载器语法为://ReflectTest类的内加载器
ReflectTest.class.getClassLoader();
在Eclipse中,将配置文件config.properties保存到工程下的src(源代码)目录下,Eclipse会自动将这些文件拷贝到项目的bin目录下面。我们在运行程序时,使用的配置文件时bin目录下的(也可以叫做class目录下),而不是下图所示的目录:
下面我们使用内加载器来管理配置文件,修改上一节的代码(只显示主函数,ReflectPoint类在25.2节中,配置文件的内容在26章中):
public class ReflectTest {
public static void main(String[] args) throws Exception {
//ReflectTest类的内加载器,将配置文件加载进来,配置文件保存在bin/com/itheima/day25/目录下,getResourceAsStream方法表示加载资源
InputStream in = ReflectTest.class.getClassLoader().getResourceAsStream("com/itheima/day25/config.properties");
Properties pro = new Properties();
//加载
pro.load(in);
in.close();
//获取键对应的值
String className = pro.getProperty("className");
//创建实例对象
Collection collection = (Collection) Class.forName(className).newInstance();
ReflectPoint pt1 = new ReflectPoint(3, 3);
ReflectPoint pt2 = new ReflectPoint(5, 5);
ReflectPoint pt3 = new ReflectPoint(3, 3);
collection.add(pt1);
collection.add(pt1);
collection.add(pt2);
collection.add(pt3);
//集合大小:4
System.out.println(collection.size());
}
}
以后学习的框架Struct和Spring等,配置文件都是保存在classpath目录下(项目的bin目录下),因为这些框架都使用内加载器。