问题

  1. public class ZhiXingShunXu{
  2. public static void main(String[] args){
  3. System.out.println("1");
  4. new D();
  5. }
  6. }
  7. class C {
  8. static{System.out.println("2");}
  9. C(){System.out.println("3");}
  10. {System.out.println("4");}
  11. }
  12. class D extends C{
  13. public String sd1=getSd1();
  14. static public String getSd(){
  15. System.out.println("5");
  16. return "sd";
  17. }
  18. static{System.out.println("6");}
  19. public static String sd=getSd();
  20. D(){System.out.println("7");}
  21. {System.out.println("8");}
  22. public String getSd1(){
  23. System.out.println("9");
  24. return "sd1";
  25. }
  26. }

执行结果为何是:126543987

看完下面的“子类实例化过程”就能理解了,不过要注意的是,初始化时执行的是static语句和代码块,对于static函数并不会执行。

  • 问题2:关于廖雪峰异常处理-使用Commons Logging一节末尾为何子类可以直接使用父类的log实例?

    由于Java类的动态特性,子类获取的log字段实际上相当于LogFactory。getLog(Student.class)


java基础

变量

对于float类型,需要加上f后缀。
Java语言对布尔类型的存储并没有做规定,因为理论上存储布尔类型只需要1 bit,但是通常JVM内部会把boolean表示为4字节整数。

操作符

&&&

分别称为长路与和短路与。需要注意的是:长路与 两侧,都会被运算。短路与 只要第一个是false,第二个就不进行运算了。

  1. boolean b = !(i++ == 3) ^ (i++ ==2) && (i++==3); // b = !(false) ^ (ture) && (不执行)

++--

需要注意的是:i++:先取值,再运算。++i:先运算,再取值。

  1. # example1
  2. int i = 1;
  3. System.out.println(i++); //输出1
  4. int j = 1;
  5. System.out.println(++j); //输出2
  6. # example2
  7. int i = 1;
  8. i = i + (++i);
  9. System.out.print(i); //输出3
  10. # example3
  11. int i = 1;
  12. i = (++i) + i;
  13. System.out.println(i); //输出4
  14. # example4
  15. int i = 1
  16. i += ++i;
  17. System.out.println(i); //输出3,因为i += ++i等价于i = i+(++i)

流程控制

break、continue的用法

  • break主要用于switch和for语句中,跳出当前循环或判断,尤其swtich语句中若无break,满足条件的case语句和后续语句都将被执行。
  • continue语句用来结束当前循环,并进入下一次循环,即仅仅这一次循环结束了,不是所有循环结束了,后边的循环依旧进行。

除此以外,break和continue语句可以和冒号标记结合使用创造便利。
如:

  1. # break跳出多层循环
  2. first:
  3. for(int j=0; j<5; j++){
  4. second:
  5. for(int i=0; i<5; i++){
  6. if(i == 0){
  7. System.out.println(i);
  8. break first;
  9. }
  10. }
  11. System.out.println("跳出1层for循环到这啦");
  12. if(j == 0){
  13. System.out.println("终结者");
  14. break;
  15. }
  16. }
  17. # continue跳过一次多层循环
  18. outer1:
  19. for(int i =0;i<4;i++){
  20. System.out.println("begin to itrate. "+i);
  21. for(int j =0;j<2;j++){
  22. if(i==2){
  23. // continue outer1;
  24. break;
  25. }
  26. System.out.println("now the value of j is:"+j);
  27. }
  28. System.out.println("******************");
  29. }

子类实例化过程

过程大概总结起来就是两个过程:1、类的初始化:jvm首先会将子类和父类的静态语句、代码块收集起来,按顺序执行。2.实例初始化:jvm将父类的普通语句和代码块按顺序移入其构造函数的顶部,然后执行构造函数。然后在子类中重复这一过程。

举个栗子:

普通版

  1. public class HelloWorld {
  2. public static void main(String[] args) {
  3. Person stu = new Student("xioaming");
  4. }
  5. }
  6. class Person{
  7. protected String fathername="father";
  8. public Person(String ss){
  9. System.out.println("1");
  10. }
  11. {
  12. System.out.println(this.fathername);
  13. System.out.println("2");
  14. }
  15. }
  16. class Student extends Person{
  17. protected String name;
  18. public Student(String name){
  19. super(name);
  20. System.out.println("3");
  21. this.name = "hhh";
  22. }
  23. }
  24. # output
  25. father
  26. 2
  27. 1
  28. 3

由此可见,当实例化一个子类时,首先执行子类的构造方法(#20),然后执行父类构造方法super()(#21,即使没有写出来,编译器也会自动加上super(),但如果父类没有无参构造方法,那么就会报错,所以建议明确的写出来)。执行super()时,先执行父类的变量声明(#8),然后执行实例代码块J(#12-15),然后才是父类构造方法(#9-11),完成以后跳转到子类构造方法中继续执行(#22)。 :::danger 有关静态代码块的执行顺序有待补充。#static ::: 参考:Java中类的实例化过程变量的初始化顺序,以及常见笔试程序阅读题分析

进阶版

如果类中包含有静态字段(类变量)和静态代码块,那么情况将会复杂一些。

  1. public class HelloWorld {
  2. public static void main(String[] args) {
  3. Person p1 = new Student();
  4. p1.haha();
  5. Person p2 = new Student();
  6. p2.haha();
  7. }
  8. }
  9. abstract class Person {
  10. static int a=1;
  11. public void haha() {
  12. System.out.println("father: instance'method");
  13. }
  14. {System.out.println("father: normal code and static arg:a= "+a);}
  15. static{System.out.println("father: static code and static arg:a= "+a);}
  16. public Person() {
  17. System.out.println("father: construct father");
  18. }
  19. }
  20. class Student extends Person {
  21. static int b=1;
  22. static{System.out.println("son: static code and static arg:b= "+b);}
  23. {System.out.println("son: normal code and static arg:b= "+b);}
  24. public Student() {
  25. System.out.println("son: construct son");
  26. }
  27. }
  28. # output
  29. father: static code and static arg:a= 1
  30. son: static code and static arg:b= 1
  31. father: normal code and static arg:a= 1
  32. father: construct father
  33. son: normal code and static arg:b= 1
  34. son: construct son
  35. father: instance'method
  36. father: normal code and static arg:a= 1
  37. father: construct father
  38. son: normal code and static arg:b= 1
  39. son: construct son
  40. father: instance'method

java类加载共有几个阶段,但真正影响结果可以归为两部分:
第一部分,在JVM完成对Class字节码文件的校验之后,JVM就会开始为类变量(static所修饰)分配内存并初始化。值得注意的是,这里的初始化是指为变量赋予对应的默认值(char为null,数值型为0,布尔为false),而不是用户代码里初始化的值。但是,如果该变量被final所修饰,如public static final int n = 1;则n将在此环节直接被赋值为1。原因在于final的特性:仅可赋值一次。
第二部分,JVM将根据需要动态地进行类的初始化。这里要注意初始化与执行构造函数的区别。初始化时JVM将会收集类中的静态变量赋值语句(比如上面第一部分未执行的半句赋值语句)和静态代码块组成初始化方法并执行(比如类Person中的#12行的赋值部分和#17行)。那什么情况下会进行类的初始化呢?有以下几种情况:

  1. 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

常见的,比如通过new进行实例化,通过.访问类静态变量/方法时,如果类之前没有初始化过,那么此时就会执行。

所以,我们看到上面程序中,当我们实例化子类时,在实例化之前(还未分配内存),先初始化了父类(执行了静态变量的赋值语句和静态代码块),然后执行了子类的初始化。但在整个程序运行期间,类初始化的过程只有一次,但普通代码块和构造函数却在每次实例化时都执行了一遍(可以将类中代码分为两块,static修饰的是类的,初始化时执行的。而除此以外的代码,都是实例的,每次实例化都会执行)。

:::info 那么问题来了,如果在上面程序的#7行加上一句static{System.out.println(Student.a);},结果会有什么变化?
为什么子类的静态代码块没有执行?没有初始化?
原因在于,虽然代码是Student.a,但是我们都明白GTR不是儿子的,所以JVM实际上执行的是Person.a,Student并没有初始化的必要。 :::

加强版

  1. public class Book {
  2. public static void main(String[] args)
  3. {
  4. staticFunction();
  5. }
  6. static Book book = new Book();
  7. static
  8. {
  9. System.out.println("书的静态代码块");
  10. }
  11. {
  12. System.out.println("书的普通代码块");
  13. }
  14. Book()
  15. {
  16. System.out.println("书的构造方法");
  17. System.out.println("price=" + price +",amount=" + amount);
  18. }
  19. public static void staticFunction(){
  20. System.out.println("书的静态方法");
  21. }
  22. int price = 110;
  23. static int amount = 112;
  24. }
  25. # output
  26. 书的普通代码块
  27. 书的构造方法
  28. price=110,amount=0
  29. 书的静态代码块
  30. 书的静态方法

这个例子多出了一个难点,就是初始化过程中遇到同样会触发初始化的实例化方法该如何继续?
执行过程:按照进阶例子中讲的第四点,执行main入口函数前先进行主类的初始化(仅仅是主类的初始化,也即静态语句和代码块,无需执行构造函数),于是执行#7实例化Book类。但实例化类前也是需要初始化类的,可现在初始化还未结束,该如何继续进行?要么新开栈重新初始化类导致死锁,要么继续#7处,执行构造函数,显然JVM应该选择后者。所以下一步执行实例初始化的相关代码:普通代码块和构造函数。实例化完成后回到#7之后执行静态代码块和最后的半句赋值语句(初始化前静态变量已经被分配了内存空间)。

匿名子类

常常会看到这样的代码:

  1. InvocationHandler handler = new InvocationHandler() {
  2. @Override
  3. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  4. System.out.println(method);
  5. if (method.getName().equals("morning")) {
  6. System.out.println("Good morning, " + args[0]);
  7. }
  8. return null;
  9. }
  10. };

还有这样的:

  1. List<String> list = new ArrayList<String>() {
  2. {
  3. add("a");
  4. add("b");
  5. }
  6. };

我的理解:
等号右边是新建一个ArrayList类的实例,这个类的定义就是后面的花括号所定义。这里的泛型。由于ArrayList实现了List<T>的接口,即ArrayList是List的子类,此处也就是常见的向上转型(大包小可以,小包大则不行,所以向下转型不允许)。

以上述为例,援引oschina用户:tcxu的回答:

  1. 操作符 new 表明要 创建一个 List 类型的 对象/变量 list。
  2. list里面存放的是一个数组列表(ArrayList) 对象,其下标,和普通数组的下标一样,是 int 类型,list的元素都是String类型
  3. ArrayList这个类,实现了接口 List, 故可以将 ArrayList的任何对象的类型,如 list, 看作是 java.util.List 类型。
  4. 尖括号是泛型操作符。其中的参数 String 表明List 类型的 对象的元素类型均为字符串 String 类型。
  5. 一对里面无内容的圆括号表明被调用的构造方法无需参数,就是说,被调用的构造方法是缺省的/默认的。
  6. 内层花括号显示这个类定义里的代码块,其操作为:调用ArrauList类的成员方法 add(),先后将字符串 “a” 和 “b”, 添加到此列表的尾部。只要调用这个类的构造方法,这个代码块都会被执行一次。
  7. 删掉所有的花括号: List list = new ArrayList();或仅删除内花括号: List list = new ArrayList(){} ; 结果都是: 创建了一个空的(不含字符串元素的) ArrayList 对象 list。
  8. 楼主出示的代码,其效果等同于:
    1. List<String> list = new ArrayList<String>();
    2. list.add("a");
    3. list.add("b");

回答问题:后面加两个大括号(花括号)是什么用法?

  1. 外层花括号显示,它囊括的是 数组列表类 ArrayLIst 的定义。这个花括号的用法和一个类的定义一样,比如:class Person { ….. } 。由于 ArrayList 的原有定义已经写在 java.util.ArrayList 里,这里就不必重复了。
  2. 内嵌的花括号 封装着 多行(这里是2 行)代码, 形成了一个类定义内的、独立的代码区。效果相当于,在ArrayList原有定义的基础之上,添加了这个代码块。这个镶嵌在类定义的代码块,在调用该类的任何一个(有参数的,或无参数的)构造方法时,都会被执行一次。
  3. 这个内嵌的代码块,调用的是该类的普通成员方法 add(String s),因此,它未能冠以 关键词 static.
  4. 省略这个内嵌的花括号,而仅保留其中的代码,是无法通过编译的。这就相当于在类定义里直接书写代码一样,是不允许的。因为类定义仅包括:属性(成员变量或常量)的声明,方法的声明,和 代码块。

向上/向下转型

以上面代码段定义的两个类为例:

  1. # 向上转型
  2. Person p1 = new Student();
  3. System.out.println(p1 instanceof Student); // true,所以p1实际上是个student实例
  4. Systemout.println(p1 instanceof Person); // true
  5. # 向下转型
  6. Student s1 = new Person(); // 会报错
  7. # 另一种向下转型
  8. Student s2 = new Student();
  9. p1 = s2; // 可以执行,因为p1其实就是Student实例,所以是可以装载s2的内容

向上转型是可以的,可以这么理解:向上转型时实例化创建的是子类对象,而子类本身就属于父类(如同#4所示)。但反过来却不成立,父类实例对象显然不属于子类。可以这么记忆,我们常说:“来了个人,一个学生”,而不会说“来了个学生,一个人”。

方法重载(overload)与覆写(override)

名称、返回值类型等都相同但参数不同的方法,谓之曰方法重载,是为了达到一个方法名按需调用不同方法的便利,所以这些方法都定义在同一个类中。
子类定义的与父类名称、返回值类型、参数均相同的方法,谓之曰覆写。需要注意的是,由于子类是无法访问父类中的私有方法,所以如果子类中的方法与父类中的私有方法同名,这种情况并非覆写。详见下例:

  1. public class Test {
  2. public static void main(String[] args) {
  3. new A().printPerson();
  4. new B().printPerson();
  5. }
  6. }
  7. class A {
  8. public void printPerson() {
  9. System.out.println(getInfo());
  10. }
  11. private String getInfo() {
  12. return "A";
  13. }
  14. }
  15. class B extends A{
  16. public String getInfo() {
  17. return "B";
  18. }
  19. # output
  20. A
  21. A

如果将class AgetInfo()的private换成protected或者public,运行结果就是A B了。

private、protected、public和default的区别

语雀内容

抽象类与接口

这部分可以参考菜鸟教程,非常清晰
当一个类被声明为抽象类,它将无法被实例化。如果类中还声明了抽象方法,则其子类要么也是抽象类要么必须覆写该抽象方法。

接口可以理解为抽象类的抽象类,只包含抽象方法和public+static+final共同修饰的变量(字段)。正是这么的“纯粹”,所以所有的抽象方法可以省略前缀public abstract,所有的变量可以省略public+static+final

实例字段与静态字段

首先明确两个等价概念,字段=变量,方法=属性,后面统一以字段、方法谓之。以目前的理解来看,类中代码除了方法就是字段,而通过前缀加不加static修饰符又可以分为实例方法/字段(有时也称为类成员方法/变量)和静态方法/字段(类方法/变量)。
实例字段,顾名思义,实例化时才分配内存空间,通过实例+.的方式进行访问。
静态字段,则在程序集装载时就已分配内存完成初始化。 :::info 形如static{}这样的,称之为静态代码块,与上方的静态方法/字段加载机制相同。 :::