[关闭]
@adamhand 2019-03-26T03:43:44.000000Z 字数 36132 阅读 690

Java基础(补充)


一、数据类型

1. int和Integer的区别

1.1 int和Integer的基本使用对比

(1)Integer是int的包装类;int是基本数据类型;
(2)Integer变量必须实例化后才能使用;int变量不需要;
(3)Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值 ;
(4)Integer的默认值是null;int的默认值是0。

1.2 int和Integer的深入比较

(1)由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。

  1. Integer i = new Integer(100);
  2. Integer j = new Integer(100);
  3. System.out.print(i == j); //false

(2)Integer变量和int变量比较时,只要两个变量的值是相等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)

  1. Integer i = new Integer(100);
  2. int j = 100
  3. System.out.print(i == j); //true

(3)非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象(前提是在-128-127之间,超过这个会new对象),而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)

  1. Integer i = new Integer(100);
  2. Integer j = 100;
  3. System.out.print(i == j); //false

(4)对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false。原因是Integer 缓存池的大小默认为 -128~127。

  1. Integer i = 100;
  2. Integer j = 100;
  3. System.out.print(i == j); //true
  4. Integer i = 128;
  5. Integer j = 128;
  6. System.out.print(i == j); //false

1.3 自动装箱和自动拆箱

(1)自动装箱:将基本数据类型重新转化为对象

  1. public class Test {
  2. public static void main(String[] args) {
  3. //声明一个Integer对象
  4. Integer num = 9;
  5. //以上的声明就是用到了自动的装箱:解析为:Integer num = Integer.valueOf(9);
  6. }
  7. }

(2)自动拆箱:将对象重新转化为基本数据类型

  1. public class Test {
  2. public static void main(String[] args) {
  3. //声明一个Integer对象
  4. Integer num = 9;
  5. //进行计算时隐含的有自动拆箱
  6. System.out.print(num--);
  7. }
  8. }

因为对象时不能直接进行运算的,而是要转化为基本数据类型后才能进行加减乘除。对比:

  1. //装箱
  2. Integer num = 10;
  3. //拆箱
  4. int num1 = num;

小结1:使用Integer产生变量时有以下三种方式:

  1. Integer num = new Integer(100); //堆中
  2. Integer num = 100; //可能在缓存池中(-128-127之间时;超过就new对象)
  3. Integer num = Integer.valueOf(100)//可能在缓存池中(-128-127之间时;超过就new对象)

valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。

  1. public static Integer valueOf(int i) {
  2. if (i >= IntegerCache.low && i <= IntegerCache.high)
  3. return IntegerCache.cache[i + (-IntegerCache.low)];
  4. return new Integer(i);
  5. }

注意,不仅是Integer变量能用缓存池,Byte,Short,Long,Character也能用。

小结2:Integer缓存池和Java常量池

  前面好像“缓存池”的概念和“常量池”混用了,其实缓存池就是常量池技术的一种实现。缓存池其实是利用一个Integer类型的数组来实现的,而这个数组被声明为final类型,所以放在常量池中。见下面的程序:

  1. private static class IntegerCache {
  2. static final int high;
  3. static final Integer cache[];
  4. static {
  5. final int low = -128;
  6. // high value may be configured by property
  7. int h = 127;
  8. if (integerCacheHighPropValue != null) {
  9. // Use Long.decode here to avoid invoking methods that
  10. // require Integer's autoboxing cache to be initialized
  11. int i = Long.decode(integerCacheHighPropValue).intValue();
  12. i = Math.max(i, 127);// Maximum array size is Integer.MAX_VALUE
  13. h = Math.min(i, Integer.MAX_VALUE - -low);
  14. }
  15. high = h;
  16. cache = new Integer[(high - low) + 1];
  17. int j = low;
  18. for(int k = 0; k < cache.length; k++)
  19. cache[k] = new Integer(j++);
  20. }
  21. private IntegerCache(){}
  22. }

小结3:Integer和int在内存中的哪里

  1. int a= 200;
  2. Integer b = new Integer(200);
  3. Integer c= 200;

二、String

1. 关于String不可变

1.1 什么是对象不可变

可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。

1.2 区分对象和对象的引用

先看如下代码:

  1. String s = "ABCabc";
  2. System.out.println("s = " + s);
  3. s = "123456";
  4. System.out.println("s = " + s);

打印结果为:

  1. s = ABCabc
  2. s = 123456

从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢? 其实这里存在一个误区: s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。如下图所示:



1.3 String、StringBuffer和StringBuilder

1.3.1 可变与不可变

String类中使用字符数组保存字符串,如下就是,因为有“final”修饰符,所以可以知道string对象是不可变的。

  1. private final char value[];

StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,如下就是,可知这两种对象都是可变的。

  1. char[] value;

1.3.2 是否多线程安全


注意:

对String的操作无论是sub操、concat还是replace操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。
所以要永远记住一点:
“对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象”。


1.3.3 深入理解String、StringBuffer和StringBuilder

(1)String str="hello world"String str=new String("hello world")的区别

例子:

  1. public class Main {
  2. public static void main(String[] args) {
  3. String str1 = "hello world";
  4. String str2 = new String("hello world");
  5. String str3 = "hello world";
  6. String str4 = new String("hello world");
  7. System.out.println(str1==str2);
  8. System.out.println(str1==str3);
  9. System.out.println(str2==str4);
  10. }
  11. }

输出结果:

  1. false
  2. true
  3. false

原因:
  在class文件中有一部分 来存储编译期间生成的 字面常量以及符号引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池

  因此在上述代码中,String str1 = "hello world";String str3 = "hello world"; 都在编译期间生成了字面常量和符号引用,运行期间字面常量"hello world"被存储在运行时常量池(当然只保存了一份)。通过这种方式来将String对象跟引用绑定的话,JVM执行引擎会先在运行时常量池查找是否存在相同的字面常量,如果存在,则直接将引用指向已经存在的字面常量;否则在运行时常量池开辟一个空间来存储该字面常量,并将引用指向该字面常量。

  众所周知,通过new关键字来生成对象是在堆区进行的,而在堆区进行对象生成的过程是不会去检测该对象是否已经存在的。因此通过new来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的。

(2)StringStringBuffer以及StringBuilder的区别

  既然在Java中已经存在了String类,那为什么还需要StringBuilder和StringBuffer类呢?看代码:

  1. public class Main {
  2. public static void main(String[] args) {
  3. String string = "";
  4. for(int i=0;i<10000;i++){
  5. string += "hello";
  6. }
  7. }
  8. }

  这句 string += "hello";的过程相当于将原有的string变量指向的对象内容取出与"hello"作字符串相加操作再存进另一个新的String对象当中,再让string变量指向新生成的对象。每次循环会new出一个StringBuilder对象,然后进行append操作,最后通过toString方法返回String对象。也就是说这个循环执行完毕new出了10000个对象,试想一下,如果这些对象没有被回收,会造成多大的内存资源浪费。
  而使用StringBuilder时new操作只进行了一次,也就是说只生成了一个对象,append操作是在原有对象的基础上进行的。

(3)三者对比

  1)对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如"I"+"love"+"java"; 的字符串相加,在编译期间便被优化成了"Ilovejava"。这个可以用javap -c命令反编译生成的class文件进行验证。

  对于间接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。

  2)String、StringBuilder、StringBuffer三者的执行效率:

  1. StringBuilder > StringBuffer > String

(4)常见面试试题

  1. String a = "hello2";   
  2. String b = "hello" + 2;   
  3. System.out.println((a == b));

  输出结果为:true。原因很简单,"hello"+2在编译期间就已经被优化成"hello2",因此在运行期间,变量a和变量b指向的是同一个对象。

  1. String a = "hello2";  
  2. String b = "hello";
  3. String c = b + 2;
  4. System.out.println((a == c));

  输出结果为:false。由于有符号引用(即字符串引用)的存在,所以 String c = b + 2;不会在编译期间被优化,不会把b+2当做字面常量来处理的,因此这种方式生成的对象事实上是保存在堆上的。

  1. String a = "hello2";  
  2. final String b = "hello";
  3. String c = b + 2;
  4. System.out.println((a == c));

  输出结果为:true。对于被final修饰的变量,会在class文件常量池中保存一个副本,也就是说不会通过连接而进行访问,对final变量的访问在编译期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = "hello" + 2;

  1. public class Main {
  2. public static void main(String[] args) {
  3. String a = "hello";
  4. String b = new String("hello");
  5. String c = new String("hello");
  6. String d = b.intern();
  7. System.out.println(a==b);
  8. System.out.println(b==c);
  9. System.out.println(b==d);
  10. System.out.println(a==d);
  11. }
  12. }

结果为(jdk1.6、jdk1.8):

  1. false
  2. false
  3. false
  4. true

  这里面涉及到的是String.intern方法的使用。在String类中,intern方法是一个本地方法,在JAVA SE6之前,intern方法会在运行时常量池中查找是否存在内容相同的字符串,如果存在则返回指向该字符串的引用,如果不存在,则会将该字符串入池,并返回一个指向该字符串的引用。因此,a和d指向的是同一个对象。

  而这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念 该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。

  因此,这个问题如果换成 String str = new String("abc")涉及到几个String对象?合理的解释是2个。

  个人觉得在面试的时候如果遇到这个问题,可以向面试官询问清楚”是这段代码执行过程中创建了多少个对象还是涉及到多少个对象“再根据具体的来进行回答。

  1. public class Main {
  2. public static void main(String[] args) {
  3. String str1 = "I";
  4. //str1 += "love"+"java"; 1)
  5. str1 = str1+"love"+"java"; //2)
  6. }
  7. }

  1)的效率比2)的效率要高,1)中的"love"+"java"在编译期间会被优化成"lovejava",而2)中的不会被优化。在1)中只进行了一次append操作,而在2)中进行了两次append操作。可以使用javap -c 类名来查看字节码,比如javap -c Main

1.4 String#intern

1.4.1 intern实现原理

  “如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。

1.4.2 jdk6 和 jdk7 下 intern 的区别

  看一段代码:

  1. public static void main(String[] args) {
  2. String s = new String("1");
  3. s.intern();
  4. String s2 = "1";
  5. System.out.println(s == s2);
  6. String s3 = new String("1") + new String("1");
  7. s3.intern();
  8. String s4 = "11";
  9. System.out.println(s3 == s4);
  10. }

  打印结果是:

  1. public static void main(String[] args) {
  2. String s = new String("1");
  3. String s2 = "1";
  4. s.intern();
  5. System.out.println(s == s2);
  6. String s3 = new String("1") + new String("1");
  7. String s4 = "11";
  8. s3.intern();
  9. System.out.println(s3 == s4);
  10. }

  打印结果为:

jdk6中的解释


注:图中绿色线条代表 string 对象的内容指向。 黑色线条代表地址指向。

  在 jdk6中上述的所有打印都是 false 的,因为 jdk6中的常量池是放在 Perm 区中的,Perm 区和正常的 JAVA Heap 区域是完全分开的。上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern方法也是没有任何关系的。

jdk7中的解释

  在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生java.lang.OutOfMemoryError: PermGen space错误的。 所以在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。为什么要移动,Perm 区域太小是一个主要原因,当然据消息称 jdk8 已经直接取消了 Perm 区域,而新建立了一个元区域。应该是 jdk 开发者认为 Perm 区域已经不适合现在 JAVA 的发展了。

小结

  从上述的例子代码可以看出 jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括2点:

1.5 从字符串比较到==和equals方法区别

首先看一段代码:

  1. String str1 = new String("hello");
  2. String str2 = "hello";
  3. System.out.println("str1==str2: " + (str1==str2)); \\1
  4. System.out.println("str1.equals(str2): " + str1.equals(str2)); \\2

打印结果为:
false
true
原因:
String类中,==比较的是两个元素的地址,代码中一个在堆中,一个在常量池中,显然不同;equals()方法首先比较两个字符串的长度,长度不同则返回false,否则再比较字符串的每一位,不同返回false,若每一位都相同,则返回true。“比较每一位”的操作是通过一个char类型的数组完成的,如下图所示:

说完了String,再看一下通用的==equals的区别。
java中的数据类型,可分为两类:
1.基本数据类型,也称原始数据类型。byte,short,char,int,long,float,double,boolean
它们之间的比较,应用双等号(==),比较的是它们的值。
2.复合数据类型(类)
当它们用(==)进行比较的时候,比较的是它们在内存中的存放地址,所以,除非是同一个new出来的对象,比较后的结果为true,否则比较后结果为false。 JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地址,但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了。
对于复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals方法也是用双等号(==)进行比较的,所以比较后的结果跟双等号(==)的结果相同。

三、运算

1. switch

switch支持的类型:

参数传递

首先说明,Java 语言的参数传递只有「按值传递」

然而我们经常看到对于对象(数组,类,接口)的传递似乎有点像引用传递,可以改变对象中某个属性的值。但是不要被这个假象所蒙蔽,实际上这个传入函数的值是对象引用的拷贝,即传递的是引用的地址值,是将对象的地址以值的方式传递到形参中,方法得到的是所有参数值的一个拷贝,所以还是按值传递。

方法参数共有两种类型:

基本数据类型为参数

先看一段代码:

  1. public static void main(String[] args) {
  2. int num = 1;
  3. System.out.println(num);
  4. changeNumber(num);
  5. System.out.println(num);
  6. }
  7. private static void changeNumber(int x){
  8. x++;
  9. }

打印结果为:

  1. 1
  2. 1

以上程序的执行过程为:

引用为参数

  1. public static void main(String[] args) {
  2. int[] nums = {3,2,1};
  3. changeNums(nums);
  4. for(int i = 0; i < nums.length; i++)
  5. System.out.print(nums[i] + " ");
  6. }
  7. private static void changeNums(int[] nums){
  8. for(int i = 0; i < nums.length; i++)
  9. nums[i]++;
  10. }

打印结果为:

  1. 4 3 2

可以看到,调用changeNums函数之后,nums数组的值被改变了。

以上程序的执行过程为:

再看另一个例子:

  1. public class Dog {
  2. String name;
  3. Dog(String name) {
  4. this.name = name;
  5. }
  6. String getName() {
  7. return this.name;
  8. }
  9. void setName(String name) {
  10. this.name = name;
  11. }
  12. String getObjectAddress() {
  13. return super.toString();
  14. }
  15. }
  16. public class PassByValueExample {
  17. public static void main(String[] args) {
  18. Dog dog = new Dog("A");
  19. System.out.println(dog.getObjectAddress()); // Dog@4554617c
  20. func(dog);
  21. System.out.println(dog.getObjectAddress()); // Dog@4554617c
  22. System.out.println(dog.getName()); // A
  23. }
  24. private static void func(Dog dog) { //调用该方法后,dog被初始化为A第一个拷贝
  25. System.out.println(dog.getObjectAddress()); // Dog@4554617c
  26. dog = new Dog("B"); //将A的拷贝指向另一个对象,对A没有任何影响
  27. System.out.println(dog.getObjectAddress()); // Dog@74a14482
  28. System.out.println(dog.getName()); // B
  29. }
  30. }

如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。

  1. class PassByValueExample {
  2. public static void main(String[] args) {
  3. Dog dog = new Dog("A");
  4. func(dog);
  5. System.out.println(dog.getName()); // B
  6. }
  7. private static void func(Dog dog) {
  8. dog.setName("B");
  9. }
  10. }

参考:
java的值传递和引用传递的问题
java值传递还是引用传递
Java的参数传递是「按值传递」还是「按引用传递」?


四、继承

1. 继承关系图



2. 抽象类和接口

2.1 为什么接口中的成员变量必须是public static final的?

  首先明白一个原理,就是接口的存在意义。接口就是为了实现多继承的抽象类,是一种高度抽象的模板、标准或者说协议。规定了什么东西该是这样,如果你继承了我这接口,就必须这样。比如USB接口,就是小方口,两根电源线和两根数据线,不能多不能少。
(1)public
  既然是公共的模板或者协议,那么如果定义成private就没有意义了,因为所有继承了你这接口的类都不能用,并且接口中的方法是不能够被具体实现的,因此,接口内部中也没有任何方法可使用。为了让所有实现了该接口的类能够使用,就必须是public的。接口中定义的所有东西就应该是对所有用户开放的东西。

(2)static
  如果接口中的成员变量是非静态的,那么每一个实现了该接口的类都会有这么一个变量。那么,因为接口是多继承的,那么如果另一个接口也是有同样这样一个变量呢,那你用哪一个?所以,因为是标准,所以我规定从一开始,这个东西只能有一份,只能放在静态存储区,如果第二个接口也想同命名这么一个变量,那么存储时候就会报错,因为我静态存储区已经有一份了。你改名吧。

(3)final
  想想,如果不是final的,那么意味着每一个实现了该接口的子类都可以去修改这个变量。我们开头说了,接口就是标准规范,也改也只能是制定该接口的架构师来改,如果某类随便改的话,那么其他也继承了该接口的类就会受到影响。牵一发而动全身!!因此,既然是标准,那么就不能改,方便管理。

最后归纳:

3. super()和this()

这三个语句都必须放在第一行。如果什么都不写,会默认调用super();如果写了this(形参),那么会调用自己的带参构造函数,在带参构造函数中又会调用super()。例子如下:

  1. public class SuperClass {
  2. private String name;
  3. public SuperClass(){
  4. this("super");
  5. System.out.println("super class ()");
  6. }
  7. public SuperClass(String name){
  8. this.name = name;
  9. System.out.println("super class ("+name+")");
  10. }
  11. }
  12. public class SonClass extends SuperClass {
  13. public SonClass(){
  14. this("son");
  15. System.out.println("son class ()");
  16. }
  17. public SonClass(String name){
  18. System.out.println("son class ("+name+")");
  19. }
  20. }
  21. public class main {
  22. public static void main(String[] args) {
  23. SonClass sonClass = new SonClass();
  24. }
  25. }

结果如下:

  1. super class (super)
  2. super class ()
  3. son class (son)
  4. son class ()

经过上面的例子可以看出,进行子类构造函数初始化之前需要先调用父类的构造哈数,但是如果在调用子类构造函数的时候执行了一条语句,那么这条语句是先于父类构造函数调用之前执行呢还是在父类构造函数调用之后执行?答案是会先执行这条语句,然后再调用父类的构造函数。例子如下,是根据《Java编程思想》上的一个例子扩展的。

  1. public class Flower extends SuperFlower {
  2. int petalCount = 0;
  3. String s = "init value";
  4. //3
  5. Flower(int petais){
  6. this(new soutClass()); //4
  7. petalCount = petais;
  8. System.out.println("Constructor W/ int arg only. petalCount= "+ petalCount);
  9. }
  10. //2
  11. Flower(String s, int petals){
  12. this(petals);
  13. this.s = s;
  14. System.out.println("string & int args");
  15. }
  16. //1
  17. Flower(){
  18. this("hi", 47);
  19. System.out.println("default constructor (no args)");
  20. }
  21. //5
  22. Flower(soutClass sc){
  23. }
  24. void printPetalCount(){
  25. System.out.println("petalCount = "+ petalCount + " s = "+s);
  26. }
  27. public static void main(String[] args) {
  28. Flower f = new Flower();
  29. f.printPetalCount();
  30. }
  31. private static class soutClass {
  32. soutClass(){
  33. System.out.println("i am sout class");
  34. }
  35. }
  36. }

这个例子中,main函数执行时首先会调用无参构造函数1,1会调用2,2会调用3,3会调用5。3调用5的时候需要执行一条new语句示例一个soutClass的对象,所以会调用soutClass的构造函数,输出一个i am sout class语句。

这个例子的执行结果如下,可以看到,i am sout class语句先于i am super flower执行,也就是说,如果子类在调用构造函数的时候执行了一条语句,这条语句会先于父类的构造函数执行。

需要注意的是,soutClass类要被声明为static,否则会报Cannot reference XXX before supertype constructor has been called错。原因是父类构造函数初始化早于子类非静态变量的初始化,晚于子类静态变量的初始化。这条语句先于父类构造函数执行,soutClass必须为静态的。

  1. i am sout class
  2. i am super flower
  3. Constructor W/ int arg only. petalCount= 47
  4. string & int args
  5. default constructor (no args)
  6. petalCount = 47 s = hi

五、反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
Java反射机制主要提供了以下功能: 在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法;生成动态代理。

1. 反射的三种实现方式

1.1 对象名.getClass()

  1. public static void getClassDemo() {
  2. Person p = new Person();
  3. Class clazz1 = p.getClass();
  4. Person p1 = new Person();
  5. Class clazz2 = p.getClass();
  6. System.out.println(clazz1 == clazz2); //true
  7. }

1.2 类名.class

  1. public static void getClassDemo() {
  2. Class clazz1 = Person.class;
  3. Class clazz2 = Person.class;
  4. System.out.println(clazz1 == clazz2); //true
  5. }

1.3 类所在的绝对路径

  1. public static void getClassDemo() throws ClassNotFoundException {
  2. String className = "Test.Person";
  3. Class clazz = Class.forName(className);
  4. System.out.println(clazz);
  5. }

2. Java反射可以实现的功能:

3. JAVA反射API:反射API用来生成当前JAVA虚拟机中的类、接口或对象的信息

- -
Class类 反射的核心类,可以获取类的属性、方法等内容信息
Field类 Field和Method和Constructor类都定义于Java.lang.reflect包下,Field表示类的属性,可以获取和设置类中属性的值
Method类 表示类的方法,它可以用来获取类中方法的信息,或者执行方法
Constructor类 表示类的构造方法

使用反射获得所有构造方法(包括私有的,非私有的)

使用反射获得所有的 Filed 变量

使用反射执行相应的 Method

Constructor类

调用newInstance()方法会去使用该类的空参数构造函数进行初始化,如果指定的类中没有空参数的构造函数,或者要创建的类对象需要通过指定的构造函数进行初始化。这时就用到Constructor类了。Constructor 表示的是?类的构造器,调用Constructor中的newInstance()方法其实是调用的?类的构造方法。比如下面这样写,其实调用的是?类的构造函数进行的初始化:

  1. private final Constructor<? extends T> construcor;
  2. ...
  3. constructor.newInstance();

参考:
Java反射详解
Java 反射机制详解
JAVA反射详解
黑马程序员——Java基础---反射Class类、Constructor类、Field类


关于反射的进一步知识,参考下面几个链接
https://www.importnew.com/23902.html
https://mp.weixin.qq.com/s/5H6UHcP6kvR2X5hTj_SBjA
https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html
https://www.cnblogs.com/dongguacai/p/6535417.html

六、对象创建的过程

 在存在继承时,并且子类和父类中包括静态变量、静态代码块和普通代码块的时候,变量的初始过程如下:

  1. 启动JVM
  2. 将主函数所在的Test.class文件加载进入方法区中,加载过程静态内容要加载进入静态区;
  3. 执行main方法。JVM会将main方法加载到栈中,从第一行开始执行;
  4. 执行new Child()JVM会在方法区中查找是否有Child文件,如果没有就加载Child.class文件(从哪里加载呢?参见类加载过程);如果Child有直接父类,首先查找父类是否存在方法区中,如果不在要先加载父类;
Child.classParent.class中的所有的非静态内容会加载到非静态的区域中,而静态的内容会加载到静态区中。静态内容(静态变量,静态代码块,静态方法)按照书写顺序加载;
说明:类的加载只会执行一次。下次再创建对象时,可以直接在方法区中获取class信息。
  5. 开始给静态区中的所有静态的成员变量默认初始化。默认初始化完成之后,给所有的静态成员变量显示初始化;
  6. 所有静态成员变量显示初始化完成之后,开始执行静态的代码块。先执行父类的静态代码块,再执行子类的静态代码块;
  说明:静态代码块是在类加载的时候执行的,类的加载只会执行一次所以静态代码块也只会执行一次;
  非静态代码块和构造函数中的代码是在对象创建的时候执行的,因此对象创建(new)一次,它们就会执行一次。

  这时Parent.class文件 和 Child.class文件加载完成;
  7. 开始在堆中创建Child对象。给Child对象分配内存空间,其实就是分配内存地址;
  8. 为对象的成员变量进行默认初始化;
  9. 调用对象的构造方法,执行隐式三步

  (1)隐式super(),首先需要执行父类的构造函数,对父类进行构造函数初始化;
  (2)子类对象成员变量的显示初始化;
  (3)执行子类的构造代码块;
  接着才会执行子类构造代码块的剩余代码。
  在第(1)步中,父类构造函数初始化的时候也有隐式三步,因为父类继承自超类Object

  10. 将地址赋值给引用变量,对象初始化结束。
  下面是一个例子:

  1. package NewAObject;
  2. class Parent{
  3. int num = 100;
  4. static int staticNum = 101;
  5. static {
  6. System.out.println("Parent静态代码块:staticNum="+staticNum);
  7. staticNum++;
  8. }
  9. {
  10. System.out.println("Parent普通代码块:num="+num+" "+"staticNum="+staticNum);
  11. num++;
  12. staticNum++;
  13. }
  14. Parent(){
  15. super();
  16. System.out.println("Parent构造函数:num="+num+" "+"staticNum="+staticNum);
  17. num++;
  18. staticNum++;
  19. show();
  20. return;
  21. }
  22. void show(){
  23. System.out.println("Parent show函数:num="+num+" "+"staticNum="+staticNum);
  24. }
  25. }
  26. class Child extends Parent {
  27. int num = 1;
  28. static int staticNum = 2;
  29. static {
  30. System.out.println("Child静态代码块:staticNum="+staticNum);
  31. staticNum++;
  32. }
  33. {
  34. System.out.println("Child普通代码块:num+"+num+" "+"staticNum="+staticNum);
  35. num++;
  36. staticNum++;
  37. }
  38. Child(){
  39. super();
  40. //通过super初始化父类内容时,子类的成员并未显式初始化,而是父类初始化完毕时候才会进行显示初始化。
  41. System.out.println("Child构造函数:num="+num+" "+"staticNum="+staticNum);
  42. num++;
  43. staticNum++;
  44. return;
  45. }
  46. void show(){
  47. System.out.println("Child show函数:num="+ num+" "+"staticNum="+staticNum);
  48. }
  49. }
  50. public class NewAObject{
  51. public static void main(String[] args) {
  52. Child child = new Child();
  53. child.show();
  54. }
  55. }

  上述代码的执行结果为:

  1. Parent静态代码块:staticNum=101
  2. Child静态代码块:staticNum=2
  3. Parent普通代码块:num=100 staticNum=102
  4. Parent构造函数:num=101 staticNum=103
  5. Child show函数:num=0 staticNum=3
  6. Child普通代码块:num+1 staticNum=3
  7. Child构造函数:num=2 staticNum=4
  8. Child show函数:num=3 staticNum=5

上述过程可简化为:


补充:为何java中static静态数据无法访问非static数据,但是反过来却可以

由上述过程可知,类在加载的时候会初始化static变量,但是没有对非static变量声明和初始化,如果我们在static方法中调用类非static变量的话,就极有可能出错,当然java是不允许的。所以在编译阶段,就会报错。

待定的一个问题

补充:好像有一个例外,静态对象可以调用非静态方法。这是为什么呢?现在还没搞懂。


七、成员变量和局部变量

定义

  可以将变量分为成员变量静态变量局部变量,成员变量和静态变量是在类范围内定义的变量,其中成员变量又叫实例变量,不用static关键字修饰,静态变量又叫类变量,需要用static关键字修饰。局部变量是在一个方法内定义的变量。
  局部变量可分为:形参(形式参数)、方法局部变量、代码块局部变量。
  如下图所示:



区别

成员变量和局部变量的区别

成员变量和静态变量的区别

八、异常

1. throws和throw的区别

为什么有的异常throw之后还要再函数上throws一下,要不然就编译不通过而有些异常不用这样呢?
  如果一个异常是Exception,也就是编译时异常,那throw之后要么在函数上用throws一下,要么在本方法中catch一下。但是如果一个异常是RuntimeException,就不用。也就是说,如果自己定义的异常继承来自Exception,在函数中抛出时就必须声明或捕捉,如果是继承自RuntimeException,就不需要。
  可以认为checked exception就是要强制去处理这个异常(不管throws多少层,终归要在某个地方catch它);而runtime exception则没有这个限制,可以自由选择是否catch。
  例子如下:

  1. public class MyException {
  2. public static void main(String[] args) {
  3. int[] arr = {1, 2, 3, 4, 5};
  4. Demo d = new Demo();
  5. d.method(arr, -1);
  6. }
  7. }
  8. /**
  9. * 继承自Exception,如果在Demo中的method方法里抛出这个异常,那么必须在Demo的method方法上声明
  10. * 或在method方法中捕捉;如果采用声明的解决办法,那么所有调用method的方法都需要声明
  11. */
  12. class FuShuIndexException1 extends Exception{
  13. FuShuIndexException1(){
  14. }
  15. FuShuIndexException1(String msg){
  16. super(msg);
  17. }
  18. }
  19. /**
  20. * 继承自RuntimeException,如果在Demo中抛出这个异常,那么既不需要声明也不需要捕捉
  21. */
  22. class FuShuIndexException2 extends RuntimeException{
  23. FuShuIndexException2(){
  24. }
  25. FuShuIndexException2(String msg){
  26. super(msg);
  27. }
  28. }
  29. class Demo{
  30. public int method(int[] arr, int index){
  31. if(arr ==null)
  32. throw new NullPointerException("数组引用不能为空");
  33. if(index > arr.length)
  34. throw new ArrayIndexOutOfBoundsException("数组指针越界");
  35. if(index < 0)
  36. throw new FuShuIndexException2("角标为如数==负数");
  37. return arr[index];
  38. }
  39. }

九、泛型

泛型简介

泛型泛型,泛泛的类型。泛型就是一种不确定的类型。那么为什么要使用泛型?

使用泛型的好处大概有两个:

对于第一种好处,可以举个例子:

  1. public static void main(String[] args) {
  2. List list = new ArrayList();
  3. list.add(10);
  4. list.add("adamhand");
  5. String num = (String) list.get(0); //1
  6. }

上面的代码在编译时期不会出现任何问题,但是运行的时候在1处却会出现类型转换错误:

  1. Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

而使用泛型可以避免这个错误,使得错误在编译时期就会被检查,如下:

  1. public static void main(String[] args) {
  2. List<String> list = new ArrayList<>();
  3. list.add(10); //1
  4. list.add("adamhand");
  5. String num = (String) list.get(0);
  6. }

上述代码在编译的时候,1处就会报错。

对于第二种好处,使用Object似乎也可以实现,比如下面的代码:

  1. public class Stove {
  2. public static Object heat(Object food){
  3. System.out.println(food +"is done");
  4. return food;
  5. }
  6. public static void main(String[] args) {
  7. Meat meat = new Meat();
  8. meat = (Meat) Stove.heat(meat);
  9. Soup soup = new Soup();
  10. soup = (Soup) Stove.heat(soup);
  11. }
  12. }

定义了一个微波炉的类,使用微波炉的加热方法对不同的食物进行加热。这种写法看似没问题,但是存在小瑕疵:如果客户端不知道加热的具体内容是什么,在取出的时候进行强制类型转换,可能会出现错误。

而使用泛型可以解决这个问题,上面的代码可以改为如下:

  1. public class Stove {
  2. public static <T> T heat(T food){
  3. System.out.println(food +"is done");
  4. return food;
  5. }
  6. public static void main(String[] args) {
  7. Meat meat = new Meat();
  8. meat = Stove.heat(meat);
  9. Soup soup = new Soup();
  10. soup = Stove.heat(soup);
  11. }
  12. }

这样就可以免去强制类型转换了。

泛型类和泛型方法

需要注意的是,定义泛型方法的时候,需要将泛型定义在返回值之前;且静态方法无法使用类上定义的泛型

  1. /**
  2. * 泛型类
  3. * @param <T>
  4. */
  5. public class GenericTest<T> {
  6. private T value;
  7. public T getObject(){
  8. return value;
  9. }
  10. /**
  11. * 泛型方法,将泛型定义现在返回值之前。
  12. * @param w
  13. * @param <W>
  14. */
  15. public <W> void method(W w){
  16. System.out.println("method:" + w);
  17. }
  18. /**
  19. * 当方法静态时,不能访问类上定义的泛型。如果静态方法使用泛型,
  20. * 只能将泛型定义在方法上。
  21. * @param y
  22. * @param <Y>
  23. */
  24. public static <Y> void staticMethod(Y y){
  25. System.out.println("staticmethod" + y);
  26. }
  27. }

compareTo

如果需要比较两个泛型数的大小,需要使用Compareable接口:

  1. public static <M extends Comparable<M>> int countGreaterThan(M[] array, M elem){
  2. int count = 0;
  3. for(M e : array){
  4. if(e.compareTo(elem) > 0){
  5. count++;
  6. }
  7. }
  8. return count;
  9. }

调用方式如下:

  1. public static void main(String[] args) {
  2. Integer[] array = {1,2,3,4,5,6,7,8,9};
  3. GenericTest.countGreaterThan(array, 3);
  4. }

通配符

在java中,数组是可以协变的,比如dog extends Animal,那么Animal[] 与dog[]是兼容的。而集合是不能协变的,也就是说List<Animal>不是List<dog>的父类,这时候就可以用到通配符了。

主要有三种通配符的使用:

通配符 与 T 的区别

T:表示一个确定的类型。作用于模板上,用于将数据类型进行参数化,不能用于实例化对象。
?:表示一个不确定的类型。在实例化对象的时候,不确定泛型参数的具体类型时,可以使用通配符进行对象定义。

  1. < T > 等同于 < T extends Object>
  2. < ? > 等同于 < ? extends Object>

例一:定义泛型类,将key,value的数据类型进行< K, V >参数化,而不可以使用通配符。

  1. public class Container<K, V> {
  2. private K key;
  3. private V value;
  4. public Container(K k, V v) {
  5. key = k;
  6. value = v;
  7. }
  8. }

例二:实例化泛型对象,我们不能够确定eList存储的数据类型是Integer还是Long,因此我们使用List定义变量的类型。

  1. List<? extends Number> eList = null;
  2. eList = new ArrayList<Integer>();
  3. eList = new ArrayList<Long>();

无界通配符

等同于<? extends Object>

上界类型通配符(? extends E)

为什么叫上界?

表示E或者E的子类,那么E就是上限。E是父类,父类可接受子类。比如Object可以接收所有类型。

  1. List<? extends Number> eList = null;
  2. eList = new ArrayList<Integer>();
  3. Number numObject = eList.get(0); //语句1,正确
  4. Integer intObject = eList.get(0); //语句2,错误
  5. eList.add(new Integer(1)); //语句3,错误

语句1:ListeList存放Number及其子类的对象,语句1取出Number(或者Number子类)对象直接赋值给Number类型的变量是符合java规范的。

语句2:ListeList存放Number及其子类的对象,语句2取出Number(或者Number子类)对象直接赋值给Integer类型(Number子类)的变量是不符合java规范的。

语句3:ListeList不能够确定实例化对象的具体类型,因此无法add具体对象至列表中,可能的实例化对象如下。

  1. eList = new ArrayList<Integer>();
  2. eList = new ArrayList<Long>();
  3. eList = new ArrayList<Float>();

总结:上界类型通配符add方法受限,但可以获取列表中的各种类型的数据,并赋值给父类型(extends Number)的引用。也就是说,上界通配符允许取元素不允许存元素。

下界类型通配符(? super E)

为什么叫下界:

表示E或者E的父类,那么E就是下限。E是子类,子类不可以接收父类

  1. List<? super Integer> sList = null;
  2. sList = new ArrayList<Number>();
  3. Number numObj = sList.get(0); //语句1,错误
  4. Integer intObj = sList.get(0); //语句2,错误
  5. sList.add(new Integer(1)); //语句3,正确

语句1:List 无法确定sList中存放的对象的具体类型,因此sList.get获取的值存在不确定性,子类对象的引用无法赋值给兄弟类的引用,父类对象的引用无法赋值给子类的引用,因此语句错误。

语句2:同语句1。

语句3:子类对象的引用可以赋值给父类对象的引用,因此语句正确。

总结:下界类型通配符get方法受限,但可以往列表中添加各种数据类型的对象。因此如果你想把对象写入一个数据结构里,使用 ? super 通配符。限定通配符总是包括自己。


参考:
Java泛型三:通配符详解extends super
三句话总结JAVA泛型通配符(PECS)
JAVA泛型通配符T,E,K,V区别,T以及Class,Class的区别


类型擦除

泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容

这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除

看下面的代码:

  1. public class Erasure <T>{
  2. T object;
  3. public Erasure(T object) {
  4. this.object = object;
  5. }
  6. }

使用javap -c命令查看反编译的结果:

  1. public Generic.Erasure.Erasure(T);
  2. Code:
  3. 0: aload_0
  4. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  5. 4: aload_0
  6. 5: aload_1
  7. 6: putfield #2 // Field object:Ljava/lang/Object;
  8. 9: return

可以看到,在编译的时候T全被替换成了Object

那可不可以说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?

非也,看下面的代码(给上面代码中的T加上了上限):

  1. public class Erasure <T extends String>{
  2. T object;
  3. public Erasure(T object) {
  4. this.object = object;
  5. }
  6. }

反编译的结果为:

  1. public Generic.Erasure.Erasure(T);
  2. Code:
  3. 0: aload_0
  4. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  5. 4: aload_0
  6. 5: aload_1
  7. 6: putfield #2 // Field object:Ljava/lang/String;
  8. 9: return

可以看到,T没有被替换成Object而是被替换成String。于是可以得到结论:

在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T> 则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String> 则类型参数就被替换成类型上限。

类型擦除带来的局限性

类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。

理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如

  1. public class ToolTest {
  2. public static void main(String[] args) {
  3. List<Integer> list = new ArrayList<>();
  4. list.add(3);
  5. list.add("123"); //1
  6. }
  7. }

正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配,但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制。

  1. public interface List<E> extends Collection<E>{
  2. boolean add(E e);
  3. }

上面是 List 和其中的 add() 方法的源码定义。

因为 E 代表任意的类型,所以类型擦除时,add 方法其实等同于

  1. boolean add(Object obj);

那么,利用反射,我们绕过编译器去调用 add 方法。

  1. public class ToolTest {
  2. public static void main(String[] args) {
  3. List<Integer> ls = new ArrayList<>();
  4. ls.add(23);
  5. // ls.add("text");
  6. try {
  7. //getDeclaredMethods(),该方法是获取本类中的所有方法
  8. Method method = ls.getClass().getDeclaredMethod("add",Object.class);
  9. method.invoke(ls,"test");
  10. method.invoke(ls,42.9f);
  11. } catch (Exception e) {
  12. e.printStackTrace();
  13. }
  14. for ( Object o: ls){
  15. System.out.println(o);
  16. }
  17. }
  18. }

打印结果是:

  1. 23
  2. test
  3. 42.9

可以看到,利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制。


参考:
Java 泛型,你了解类型擦除吗?
Java泛型详解
《码出高效:Java开发手册》


十、关键字

final

十一、内部类

在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。内部类有四种类型:

如下所示:

  1. public class OuterClass {
  2. //成员内部类
  3. private class InstanceInnerClass{}
  4. //静态内部类
  5. private class StaticInnerClass{}
  6. public static void main(String[] args) {
  7. //匿名内部类
  8. new Thread(){}.start();
  9. new Thread(){}.start();
  10. //方法内部类
  11. class MethodClass_1{}
  12. class MethodClass_2{}
  13. }
  14. }

局部内部类和匿名内部类

需要注意的一点是,局部内部类和匿名内部类只能访问局部final变量,如下程序所示。b和a都是final变量。

需要注意的是,即使将final修饰符去掉,编译也不会出错,因为java会默认它们是final变量。可以验证的是,如果b不加final修饰,而试图在test方法中修改b的值,会编译出错。

  1. public class MethodInnerClass {
  2. public static void main(String[] args) {
  3. test(3);
  4. }
  5. public static void test(final int b) {
  6. final int a = 10;
  7. new Thread(){
  8. public void run() {
  9. System.out.println(a);
  10. System.out.println(b);
  11. }
  12. }.start();
  13. }
  14. }

那么,为什么它们只能访问final修饰的变量呢?

首先明白一点,当外部类的方法(上面程序中的test方法)结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有不再被引用时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,也就是说,内部类在访问局部变量时,会复制一份副本,内部类访问的其实是这个副本。这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的”copy”。这样就好像延长了局部变量的生命周期。

但是新的问题又出现了:将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是说,必须保证这两个变量的同步,如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?就将局部变量设置为final

当变量被final修饰时:

总结一下,为了解决生命周期不同的问题,匿名内部类备份了变量,为了解决备份变量引出的变量同步问题,外部变量要被定义成final。


参考:
《码出高效:Java编程手册》
为什么局部内部类和匿名内部类只能访问final的局部变量?
匿名内部类访问方法成员变量需要加final的原因及证明


为什么要使用内部类


参考:
Java内部类详解
Java中的内部类(成员内部类、静态内部类、局部内部类、匿名内部类)


十二、Object中的常用方法

clone()

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。

需要使用clone()函数的类必须实现Cloneable()接口,但clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。

浅拷贝

被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。”里面的对象“会在原来的对象和它的副本之间共享。

简而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象

例子:

  1. public class Student implements Cloneable {
  2. private String name;
  3. private int age;
  4. private Teacher teacher;
  5. public String getName() {
  6. return name;
  7. }
  8. public void setName(String name) {
  9. this.name = name;
  10. }
  11. public int getAge() {
  12. return age;
  13. }
  14. public void setAge(int age) {
  15. this.age = age;
  16. }
  17. public Teacher getTeacher() {
  18. return teacher;
  19. }
  20. public void setTeacher(Teacher teacher){
  21. this.teacher = teacher;
  22. }
  23. @Override
  24. protected Object clone() throws CloneNotSupportedException {
  25. //浅复制
  26. return super.clone();
  27. }
  28. }
  1. public class Teacher implements Cloneable {
  2. private String name;
  3. private int age;
  4. public String getName() {
  5. return name;
  6. }
  7. public void setName(String name) {
  8. this.name = name;
  9. }
  10. public int getAge() {
  11. return age;
  12. }
  13. public void setAge(int age) {
  14. this.age = age;
  15. }
  16. @Override
  17. protected Object clone() throws CloneNotSupportedException {
  18. return super.clone();
  19. }
  20. }
  1. public class Main {
  2. public static void main(String[] args) throws CloneNotSupportedException {
  3. Teacher teacher = new Teacher();
  4. teacher.setAge(30);
  5. teacher.setName("Bob");
  6. Student student1 = new Student();
  7. student1.setAge(15);
  8. student1.setName("Alice");
  9. student1.setTeacher(teacher);
  10. Student student2 = (Student) student1.clone();
  11. student2.setAge(16);
  12. student2.setName("Couche");
  13. System.out.println("student1 "+student1.getName());
  14. System.out.println("student1 "+student1.getAge());
  15. System.out.println("student1 "+student1.getTeacher().getName());
  16. System.out.println("student1 "+student1.getTeacher().getAge());
  17. System.out.println();
  18. System.out.println("student2 "+student2.getName());
  19. System.out.println("student2 "+student2.getAge());
  20. System.out.println("student2 "+student2.getTeacher().getName());
  21. System.out.println("student2 "+student2.getTeacher().getAge());
  22. student1.getTeacher().setName("Jam");
  23. System.out.println();
  24. System.out.println("修改老师信息后:");
  25. System.out.println("student1 "+student1.getTeacher().getName());
  26. System.out.println("student2 "+student2.getTeacher().getName());
  27. }
  28. }

结果为:

  1. student1 Alice
  2. student1 15
  3. student1 Bob
  4. student1 30
  5. student2 Couche
  6. student2 16
  7. student2 Bob
  8. student2 30
  9. 修改老师信息后:
  10. student1 Jam
  11. student2 Jam

可以看到,在student1中修改了teacher的名字后,student2中的teacher的名字也跟着改变了。可以得出结论:student1和student2中的teacher指向的是一个teacher对象。示意图如下:



深拷贝

深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。

简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。

将上述程序中Student类中的clone()函数改为如下,就实现了深拷贝。

  1. @Override
  2. protected Object clone() throws CloneNotSupportedException {
  3. //深复制
  4. Student student = (Student) super.clone();
  5. student.setTeacher((Teacher) student.getTeacher().clone());
  6. return student;
  7. }

结果如下:

  1. student1 Alice
  2. student1 15
  3. student1 Bob
  4. student1 30
  5. student2 Couche
  6. student2 16
  7. student2 Bob
  8. student2 30
  9. 修改老师信息后:
  10. student1 Jam
  11. student2 Bob

内存示意图如下:



clone()的替代方案

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数拷贝工厂或者序列化来拷贝一个对象。

使用拷贝构造函数

  1. public class CloneConstructorExample {
  2. private int[] arr;
  3. public CloneConstructorExample() {
  4. arr = new int[10];
  5. for (int i = 0; i < arr.length; i++) {
  6. arr[i] = i;
  7. }
  8. }
  9. public CloneConstructorExample(CloneConstructorExample original) {
  10. arr = new int[original.arr.length];
  11. for (int i = 0; i < original.arr.length; i++) {
  12. arr[i] = original.arr[i];
  13. }
  14. }
  15. public void set(int index, int value) {
  16. arr[index] = value;
  17. }
  18. public int get(int index) {
  19. return arr[index];
  20. }
  21. }
  1. CloneConstructorExample e1 = new CloneConstructorExample();
  2. CloneConstructorExample e2 = new CloneConstructorExample(e1);
  3. e1.set(2, 222);
  4. System.out.println(e2.get(2)); // 2

使用序列化

  1. package CloneTest.SerializableClone;
  2. import java.io.*;
  3. public class Student implements Serializable {
  4. private String name;
  5. private int age;
  6. private Teacher teacher;
  7. public String getName() {
  8. return name;
  9. }
  10. public void setName(String name) {
  11. this.name = name;
  12. }
  13. public int getAge() {
  14. return age;
  15. }
  16. public void setAge(int age) {
  17. this.age = age;
  18. }
  19. public void setTeacher(Teacher teacher){
  20. this.teacher = teacher;
  21. }
  22. public Teacher getTeacher(){
  23. return teacher;
  24. }
  25. public Object deepClone() throws IOException, ClassNotFoundException {
  26. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  27. ObjectOutputStream oos = new ObjectOutputStream(bos);
  28. oos.writeObject(this);
  29. ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
  30. ObjectInputStream ois = new ObjectInputStream(bis);
  31. return ois.readObject();
  32. }
  33. }
  1. package CloneTest.SerializableClone;
  2. import java.io.Serializable;
  3. public class Teacher implements Serializable {
  4. private String name;
  5. private int age;
  6. public String getName() {
  7. return name;
  8. }
  9. public void setName(String name) {
  10. this.name = name;
  11. }
  12. public int getAge() {
  13. return age;
  14. }
  15. public void setAge(int age) {
  16. this.age = age;
  17. }
  18. }
  1. package CloneTest.SerializableClone;
  2. import java.io.IOException;
  3. public class Main {
  4. public static void main(String[] args) throws IOException, ClassNotFoundException {
  5. Teacher teacher = new Teacher();
  6. teacher.setAge(50);
  7. teacher.setName("Bob");
  8. Student student1 = new Student();
  9. student1.setAge(15);
  10. student1.setName("Alice");
  11. student1.setTeacher(teacher);
  12. Student student2 = (Student) student1.deepClone();
  13. System.out.println();
  14. System.out.println("student2 "+student2.getName());
  15. System.out.println("student2 "+student2.getAge());
  16. System.out.println("student2 "+student2.getTeacher().getName());
  17. System.out.println("student2 "+student2.getTeacher().getAge());
  18. System.out.println();
  19. System.out.println("修改老师信息:");
  20. student1.getTeacher().setName("Job");
  21. System.out.println("student1 "+student1.getTeacher().getName());
  22. System.out.println("student2 "+student2.getTeacher().getName());
  23. }
  24. }

参考:
【Java深入】深拷贝与浅拷贝详解


十三、Lambda表达式

Lambda表达式是Java8中引入的新特性,要理解Lambda表达式,首先要理解什么是函数式接口

函数式接口

函数式接口(functional interface 也叫功能性接口)。简单来说,函数式接口是只包含一个方法的接口。比如Java标准库中的java.lang.Runnablejava.util.Comparator都是典型的函数式接口。

而Lambda表达式的作用,就是可以将函数式接口中的方法以一种简单的形式表达出来。

举个例子,在Arrays.sort()方法中自定义比较器,Java8之前的做法和使用Lambda表达式的做法分别是:

  1. Arrays.sort(nums, new Comparator<Integer>() {
  2. @Override
  3. public int compare(Integer o1, Integer o2) {
  4. if(o1 < o2)
  5. return -1;
  6. return 0;
  7. }
  8. });
  1. Arrays.sort(nums, (o1, o2) -> {
  2. if(o1 < o2)
  3. return -1;
  4. return 0;
  5. });

Lambda表达式的具体语法

Lambda表达式的基本语法:

也就是说,当只有一条expression时,{}return可以省略,但是有多条时,不能省略。

除此之外,还有以下几条规则:

参考:
java函数式编程之lambda表达式
Java 8 Lambda表达式,让你的代码更简洁
java8新特性(拉姆达表达式lambda)
【java8新特性】兰姆达表达式-1
Java8 Lambda表达式和流操作如何让你的代码变慢5倍

十五、System类中的getProperties()和getenv()

getProperty(String str) 中的 str参数如下:

变量名 作用
java.version Java运行时环境版本
java.vendor Java运行时环境供应商
java.vendor.url Java供应商的 URL
java.home Java安装目录
java.vm.specification.version Java虚拟机规范版本
java.vm.specification.vendor Java虚拟机规范供应商
java.vm.specification.name Java虚拟机规范名称
java.vm.version Java虚拟机实现版本
java.vm.vendor Java虚拟机实现供应商
java.vm.name Java虚拟机实现名称
java.specification.version Java运行时环境规范版本
java.specification.vendor Java运行时环境规范供应商
java.specification.name Java运行时环境规范名称
java.class.version Java类格式版本号
java.class.path Java类路径
java.library.path 加载库时搜索的路径列表
java.io.tmpdir 默认的临时文件路径
java.compiler 要使用的 JIT 编译器的名称
java.ext.dirs 一个或多个扩展目录的路径
os.name 操作系统的名称
os.arch 操作系统的架构
os.version 操作系统的版本
file.separator 文件分隔符(在 UNIX 系统中是“/”)
path.separator 路径分隔符(在 UNIX 系统中是“:”)
line.separator 行分隔符(在 UNIX 系统中是“/n”)
user.name 用户的账户名称
user.home 用户的主目录
user.dir 用户的当前工作目录

例子:

  1. public class GetProTest {
  2. public static void main(String[] args) {
  3. Properties properties = System.getProperties();
  4. for (String property : properties.stringPropertyNames()){
  5. System.out.println(property+" "+properties.get(property));
  6. }
  7. System.out.println(System.getProperty("java.version"));
  8. System.setProperty("jdbc.Driver", "hahahahah");
  9. System.out.println(System.getProperty("jdbc.Driver"));
  10. }
  11. }
  1. public class GetEnvTest {
  2. public static void main(String[] args) {
  3. Map<String, String> map = System.getenv();
  4. Set<Map.Entry<String, String>> entries = map.entrySet();
  5. for (Map.Entry<String, String> entry : entries){
  6. System.out.println(entry.getKey() +" "+entry.getValue());
  7. }
  8. System.out.println(System.getenv("JAVA_HOME"));
  9. }
  10. }

参考:
System.getProperty()方法获取系统变量(转 阿进的写字台)
System.getProperty()方法获取系统变量
JAVA System.getProperty() System.getenv()区别及示例

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注