问题
- 问题1:java的执行顺序(评论第二条)
public class ZhiXingShunXu{public static void main(String[] args){System.out.println("1");new D();}}class C {static{System.out.println("2");}C(){System.out.println("3");}{System.out.println("4");}}class D extends C{public String sd1=getSd1();static public String getSd(){System.out.println("5");return "sd";}static{System.out.println("6");}public static String sd=getSd();D(){System.out.println("7");}{System.out.println("8");}public String getSd1(){System.out.println("9");return "sd1";}}
执行结果为何是:126543987
看完下面的“子类实例化过程”就能理解了,不过要注意的是,初始化时执行的是static语句和代码块,对于static函数并不会执行。
- 问题2:关于廖雪峰异常处理-使用Commons Logging一节末尾为何子类可以直接使用父类的log实例?
由于Java类的动态特性,子类获取的log字段实际上相当于LogFactory。getLog(Student.class)
java基础
变量
对于float类型,需要加上f后缀。
Java语言对布尔类型的存储并没有做规定,因为理论上存储布尔类型只需要1 bit,但是通常JVM内部会把boolean表示为4字节整数。
操作符
&和&&
分别称为长路与和短路与。需要注意的是:长路与 两侧,都会被运算。短路与 只要第一个是false,第二个就不进行运算了。
boolean b = !(i++ == 3) ^ (i++ ==2) && (i++==3); // b = !(false) ^ (ture) && (不执行)
++和--
需要注意的是:i++:先取值,再运算。++i:先运算,再取值。
# example1int i = 1;System.out.println(i++); //输出1int j = 1;System.out.println(++j); //输出2# example2int i = 1;i = i + (++i);System.out.print(i); //输出3# example3int i = 1;i = (++i) + i;System.out.println(i); //输出4# example4int i = 1;i += ++i;System.out.println(i); //输出3,因为i += ++i等价于i = i+(++i)
流程控制
break、continue的用法
- break主要用于switch和for语句中,跳出当前循环或判断,尤其swtich语句中若无break,满足条件的case语句和后续语句都将被执行。
- continue语句用来结束当前循环,并进入下一次循环,即仅仅这一次循环结束了,不是所有循环结束了,后边的循环依旧进行。
除此以外,break和continue语句可以和冒号标记结合使用创造便利。
如:
# break跳出多层循环first:for(int j=0; j<5; j++){second:for(int i=0; i<5; i++){if(i == 0){System.out.println(i);break first;}}System.out.println("跳出1层for循环到这啦");if(j == 0){System.out.println("终结者");break;}}# continue跳过一次多层循环outer1:for(int i =0;i<4;i++){System.out.println("begin to itrate. "+i);for(int j =0;j<2;j++){if(i==2){// continue outer1;break;}System.out.println("now the value of j is:"+j);}System.out.println("******************");}
类
子类实例化过程
过程大概总结起来就是两个过程:1、类的初始化:jvm首先会将子类和父类的静态语句、代码块收集起来,按顺序执行。2.实例初始化:jvm将父类的普通语句和代码块按顺序移入其构造函数的顶部,然后执行构造函数。然后在子类中重复这一过程。
举个栗子:
普通版
public class HelloWorld {public static void main(String[] args) {Person stu = new Student("xioaming");}}class Person{protected String fathername="father";public Person(String ss){System.out.println("1");}{System.out.println(this.fathername);System.out.println("2");}}class Student extends Person{protected String name;public Student(String name){super(name);System.out.println("3");this.name = "hhh";}}# outputfather213
由此可见,当实例化一个子类时,首先执行子类的构造方法(#20),然后执行父类构造方法super()(#21,即使没有写出来,编译器也会自动加上super(),但如果父类没有无参构造方法,那么就会报错,所以建议明确的写出来)。执行super()时,先执行父类的变量声明(#8),然后执行实例代码块J(#12-15),然后才是父类构造方法(#9-11),完成以后跳转到子类构造方法中继续执行(#22)。
:::danger
有关静态代码块的执行顺序有待补充。#static
:::
参考:Java中类的实例化过程变量的初始化顺序,以及常见笔试程序阅读题分析
进阶版
如果类中包含有静态字段(类变量)和静态代码块,那么情况将会复杂一些。
public class HelloWorld {public static void main(String[] args) {Person p1 = new Student();p1.haha();Person p2 = new Student();p2.haha();}}abstract class Person {static int a=1;public void haha() {System.out.println("father: instance'method");}{System.out.println("father: normal code and static arg:a= "+a);}static{System.out.println("father: static code and static arg:a= "+a);}public Person() {System.out.println("father: construct father");}}class Student extends Person {static int b=1;static{System.out.println("son: static code and static arg:b= "+b);}{System.out.println("son: normal code and static arg:b= "+b);}public Student() {System.out.println("son: construct son");}}# outputfather: static code and static arg:a= 1son: static code and static arg:b= 1father: normal code and static arg:a= 1father: construct fatherson: normal code and static arg:b= 1son: construct sonfather: instance'methodfather: normal code and static arg:a= 1father: construct fatherson: normal code and static arg:b= 1son: construct sonfather: 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行)。那什么情况下会进行类的初始化呢?有以下几种情况:
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用 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并没有初始化的必要。
:::
加强版
public class Book {public static void main(String[] args){staticFunction();}static Book book = new Book();static{System.out.println("书的静态代码块");}{System.out.println("书的普通代码块");}Book(){System.out.println("书的构造方法");System.out.println("price=" + price +",amount=" + amount);}public static void staticFunction(){System.out.println("书的静态方法");}int price = 110;static int amount = 112;}# output书的普通代码块书的构造方法price=110,amount=0书的静态代码块书的静态方法
这个例子多出了一个难点,就是初始化过程中遇到同样会触发初始化的实例化方法该如何继续?
执行过程:按照进阶例子中讲的第四点,执行main入口函数前先进行主类的初始化(仅仅是主类的初始化,也即静态语句和代码块,无需执行构造函数),于是执行#7实例化Book类。但实例化类前也是需要初始化类的,可现在初始化还未结束,该如何继续进行?要么新开栈重新初始化类导致死锁,要么继续#7处,执行构造函数,显然JVM应该选择后者。所以下一步执行实例初始化的相关代码:普通代码块和构造函数。实例化完成后回到#7之后执行静态代码块和最后的半句赋值语句(初始化前静态变量已经被分配了内存空间)。
匿名子类
常常会看到这样的代码:
InvocationHandler handler = new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(method);if (method.getName().equals("morning")) {System.out.println("Good morning, " + args[0]);}return null;}};
还有这样的:
List<String> list = new ArrayList<String>() {{add("a");add("b");}};
我的理解:
等号右边是新建一个ArrayListList<T>的接口,即ArrayList是List的子类,此处也就是常见的向上转型(大包小可以,小包大则不行,所以向下转型不允许)。
以上述为例,援引oschina用户:tcxu的回答:
- 操作符 new 表明要 创建一个 List 类型的 对象/变量 list。
- list里面存放的是一个数组列表(ArrayList) 对象,其下标,和普通数组的下标一样,是 int 类型,list的元素都是String类型
- ArrayList
这个类,实现了接口 List , 故可以将 ArrayList的任何对象的类型,如 list, 看作是 java.util.List 类型。 - 尖括号是泛型操作符。其中的参数 String 表明List 类型的 对象的元素类型均为字符串 String 类型。
- 一对里面无内容的圆括号表明被调用的构造方法无需参数,就是说,被调用的构造方法是缺省的/默认的。
- 内层花括号显示这个类定义里的代码块,其操作为:调用ArrauList类的成员方法 add(),先后将字符串 “a” 和 “b”, 添加到此列表的尾部。只要调用这个类的构造方法,这个代码块都会被执行一次。
- 删掉所有的花括号: List
list = new ArrayList ();或仅删除内花括号: List list = new ArrayList (){} ; 结果都是: 创建了一个空的(不含字符串元素的) ArrayList 对象 list。 - 楼主出示的代码,其效果等同于:
List<String> list = new ArrayList<String>();list.add("a");list.add("b");回答问题:后面加两个大括号(花括号)是什么用法?
- 外层花括号显示,它囊括的是 数组列表类 ArrayLIst
的定义。这个花括号的用法和一个类的定义一样,比如:class Person { ….. } 。由于 ArrayList 的原有定义已经写在 java.util.ArrayList 里,这里就不必重复了。 - 内嵌的花括号 封装着 多行(这里是2 行)代码, 形成了一个类定义内的、独立的代码区。效果相当于,在ArrayList
原有定义的基础之上,添加了这个代码块。这个镶嵌在类定义的代码块,在调用该类的任何一个(有参数的,或无参数的)构造方法时,都会被执行一次。 - 这个内嵌的代码块,调用的是该类的普通成员方法 add(String s),因此,它未能冠以 关键词 static.
- 省略这个内嵌的花括号,而仅保留其中的代码,是无法通过编译的。这就相当于在类定义里直接书写代码一样,是不允许的。因为类定义仅包括:属性(成员变量或常量)的声明,方法的声明,和 代码块。
向上/向下转型
以上面代码段定义的两个类为例:
# 向上转型Person p1 = new Student();System.out.println(p1 instanceof Student); // true,所以p1实际上是个student实例System。out.println(p1 instanceof Person); // true# 向下转型Student s1 = new Person(); // 会报错# 另一种向下转型Student s2 = new Student();p1 = s2; // 可以执行,因为p1其实就是Student实例,所以是可以装载s2的内容
向上转型是可以的,可以这么理解:向上转型时实例化创建的是子类对象,而子类本身就属于父类(如同#4所示)。但反过来却不成立,父类实例对象显然不属于子类。可以这么记忆,我们常说:“来了个人,一个学生”,而不会说“来了个学生,一个人”。
方法重载(overload)与覆写(override)
名称、返回值类型等都相同但参数不同的方法,谓之曰方法重载,是为了达到一个方法名按需调用不同方法的便利,所以这些方法都定义在同一个类中。
子类定义的与父类名称、返回值类型、参数均相同的方法,谓之曰覆写。需要注意的是,由于子类是无法访问父类中的私有方法,所以如果子类中的方法与父类中的私有方法同名,这种情况并非覆写。详见下例:
public class Test {public static void main(String[] args) {new A().printPerson();new B().printPerson();}}class A {public void printPerson() {System.out.println(getInfo());}private String getInfo() {return "A";}}class B extends A{public String getInfo() {return "B";}# outputAA
如果将class A中getInfo()的private换成protected或者public,运行结果就是A B了。
private、protected、public和default的区别
抽象类与接口
这部分可以参考菜鸟教程,非常清晰
当一个类被声明为抽象类,它将无法被实例化。如果类中还声明了抽象方法,则其子类要么也是抽象类要么必须覆写该抽象方法。
接口可以理解为抽象类的抽象类,只包含抽象方法和public+static+final共同修饰的变量(字段)。正是这么的“纯粹”,所以所有的抽象方法可以省略前缀public abstract,所有的变量可以省略public+static+final。
实例字段与静态字段
首先明确两个等价概念,字段=变量,方法=属性,后面统一以字段、方法谓之。以目前的理解来看,类中代码除了方法就是字段,而通过前缀加不加static修饰符又可以分为实例方法/字段(有时也称为类成员方法/变量)和静态方法/字段(类方法/变量)。
实例字段,顾名思义,实例化时才分配内存空间,通过实例+.的方式进行访问。
静态字段,则在程序集装载时就已分配内存完成初始化。
:::info
形如static{}这样的,称之为静态代码块,与上方的静态方法/字段加载机制相同。
:::
