黑马程序员——基础加强(1)

------- 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目录下),因为这些框架都使用内加载器。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值