java面向对象三大特性(封装、继承、多态)

发布于:2022-12-21 ⋅ 阅读:(658) ⋅ 点赞:(0)

1隐藏和封装

1.1 理解封装

封装(Encapsulation)是面向对象的三大特征之一(另外两个是继承和多态),它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。

1.2 使用访问控制符

private(当前类访问权限):如果类里的一个成员(包括Field、方法和构造器等)使用private访问控制符来修饰,则这个成员只能在当前类的内部被访问。很显然,这个访问控制符用于修饰Field最合适,使用它来修饰Field就可以把Field隐藏在该类的内部

default(包访问权限):如果类里的一个成员(包括Field、方法和构造器等)或者一个外部类不使用任何访问控制符修饰,我们就称它是包访问权限,default访问控制的成员或外部类可以被相同包下的其他类访问。关于包的介绍请看5.4.3节。

protected(子类访问权限):如果一个成员(包括Field、方法和构造器等)使用protected访问控制符修饰,那么这个成员既可以被同一个包中的其他类访问,也可以被不同包中的子类访问。在通常情况下,如果使用protected来修饰一个方法,通常是希望其子类来重写这个方法。

public(公共访问权限):这是一个最宽松的访问控制级别,如果一个成员(包括Field、方法和构造器等)或者一个外部类使用public访问控制符修饰,那么这个成员或外部类就可以被所有类访问,不管访问类和被访问类是否处于同一个包中,是否具有父子继承关系。
在这里插入图片描述

对于外部类而言,它也可以使用访问控制符修饰,但外部类只能有两种访问控制级别:public和默认,外部类不能使用private和protected修饰,因为外部类没有处于任何类的内部,也就没有其所在类的内部、所在类的子类两个范围,因此private和protected访问控制符对外部类没有意义。

如果一个Java源文件里定义的所有类都没有使用public修饰,则这个Java源文件的文件名可以是一切合法的文件名;但如果一个Java源文件里定义了一个public修饰的类,则这个源文件的文件名必须与public修饰的类的类名相同。

基本原则
类里的绝大部分Field都应该使用private修饰,只有一些static修饰的、类似全局变量的Field,才可能考虑使用public修饰。除此之外,有些方法只是用于辅助实现该类的其他方法,这些方法被称为工具方法,工具方法也应该使用private修饰。

如果某个类主要用做其他类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不想被外界直接调用,则应该使用protected修饰这些方法。

希望暴露出来给其他类自由调用的方法应该使用public修饰。因此,类的构造器通过使用public修饰,从而允许在其他地方创建该类的实例。因为外部类通常都希望被其他类自由使用,所以大部分外部类都使用public修饰。

1.3 package、import和import static

Java引入了包(package)机制,提供了类的多层命名空间,用于解决类的命名冲突、类文件管理等问题。

        package packageName;

一旦在Java源文件中使用了这个package语句,就意味着该源文件里定义的所有类都属于这个包。位于包中的每个类的完整类名都应该是包名和类名的组合,如果其他人需要使用该包下的类,也应该使用包名加类名的组合。

        package lee;
        public class Hello
        {
            public static void main(String[] args)
            {
                  System.out.println("Hello World!");
            }
        }

上面程序中粗体字代码行表明把Hello类放在lee包空间下。把上面源文件保存在任意位置,使用如下命令来编译这个Java文件

        javac -d . Hello.java

前面已经介绍过,-d选项用于设置编译生成class文件的保存位置,这里指定将生成的class文件放在当前路径(.就代表当前路径)下。使用该命令编译该文件后,发现当前路径下并没有Hello.class文件,而是在当前路径下多了一个名为lee的文件夹,该文件夹下则有一个Hello.class文件。

位于包中的类,在文件系统中也必须有与包名层次相同的目录结构。

当虚拟机要装载lee.Hello类时,它会依次搜索CLASSPATH环境变量所指定的系列路径,查找这些路径下是否包含lee路径,并在lee路径下查找是否包含Hello.class文件。虚拟机在装载带包名的类时,会先搜索CLASSPATH环境变量指定的目录,然后在这些目录中按与包层次对应的目录结构去查找class文件。

同一个包中的类不必位于相同的目录下,例如,有lee.Person和lee.PersonTest两个类,它们完全可以一个位于C盘下某个位置,一个位于D盘下某个位置,只要让CLASSPATH环境变量里包含这两个路径即可。虚拟机会自动搜索CLASSPATH下的子路径,把它们当成同一个包下的类来处理。

为Java类添加包必须在Java源文件中通过package语句指定,单靠目录名是没法指定的。Java的包机制需要两个方面保证:① 源文件里使用package语句指定包名;② class文件必须放在对应的路径下。

为了避免不同公司之间类名的重复,Oracle建议使用公司Internet域名倒写来作为包名,例如公司的Internet域名是crazyit.org,则该公司的所有类都建议放在org.crazyit包及其子包下。

同一个包下的类可以自由访问,例如下面的HelloTest类,如果把它也放在lee包下,则这个HelloTest类可以直接访问Hello类,无须添加包前缀。

正如上面看到的,如果需要使用不同包中的其他类时,总是需要使用该类的全名,这是一件很烦琐的事情。为了简化编程,Java引入了import关键字,import可以向某个Java文件中导入指定包层次下某个类或全部类,import语句应该出现在package语句(如果有的话)之后、类定义之前。一个Java源文件只能包含一个package语句,但可以包含多个import语句,多个import语句用于导入多个包层次下的类。

        import package.subpackage...ClassName;

使用import语句导入指定包下全部类的用法如下:

        import package.subpackage...*;

上面import语句中的星号(*)只能代表类,不能代表包。

Java默认为所有源文件导入java.lang包下的所有类,因此前面在Java程序中使用String、System类时都无须使用import语句来导入这些类。但对于前面介绍数组时提到的Arrays类,其位于java.util包下,则必须使用import语句来导入该类。

静态导入使用import static语句,静态导入也有两种语法,分别用于导入指定类的单个静态Field、方法和全部静态Field、方法

使用import可以省略写包名;而使用import static则可以连类名都省略。

        import static java.lang.System.*;
        import static java.lang.Math.*;
        public class StaticImportTest
        {
            public static void main(String[] args)
            {
                  //out是java.lang.System类的静态Field,代表标准输出
                  //PI是java.lang.Math类的静态Field,表示π常量
                  out.println(PI);
                  //直接调用Math类的sqrt静态方法
                  out.println(sqrt(256));
            }
        }

2 深入构造器

构造器是一个特殊的方法,这个特殊方法用于创建实例时执行初始化。构造器是创建对象的重要途径(即使使用工厂模式、反射等方式创建对象,其实质依然是依赖于构造器),因此,Java类必须包含一个或一个以上的构造器。

2.1 使用构造器执行初始化

构造器是创建Java对象的途径,是不是说构造器完全负责创建Java对象?

不是!构造器是创建Java对象的重要途径,通过new关键字调用构造器时,构造器也确实返回了该类的对象,但这个对象并不是完全由构造器负责创建的。实际上,当程序员调用构造器时,系统会先为该对象分配内存空间,并为这个对象执行默认初始化,这个对象已经产生了——这些操作在构造器执行之前就都完成了。也就是说,当系统开始执行构造器的执行体之前,系统已经创建了一个对象,只是这个对象还不能被外部程序访问,只能在该构造器中通过this来引用。当构造器的执行体执行结束后,这个对象作为构造器的返回值被返回,通常还会赋给另一个引用类型的变量,从而让外部程序可以访问该对象。

如果用户希望该类保留无参数的构造器,或者希望有多个初始化过程,则可以为该类提供多个构造器。如果一个类里提供了多个构造器,就形成了构造器的重载

因为构造器主要用于被其他方法调用,用以返回该类的实例,因而通常把构造器设置成public访问权限,从而允许系统中任何位置的类来创建该类的对象。除非在一些极端的情况下,我们需要限制创建该类的对象,可以把构造器设置成其他访问权限,例如设置为protected,主要用于被其子类调用;把其设置为private,阻止其他类创建该类的实例。

通常建议为Java类保留无参数的默认构造器。因此,如果为一个类编写了有参数的构造器,则通常建议为该类额外提供一个无参数的构造器。

2.2 构造器重载

在这里插入图片描述

构造器B完全包含了构造器A。对于这种完全包含的情况,如果是两个方法之间存在这种关系,则可在方法B中调用方法A。但构造器不能直接被调用,构造器必须使用new关键字来调用。但一旦使用new关键字来调用构造器,将会导致系统重新创建一个对象。为了在构造器B中调用构造器A中的初始化代码,又不会重新创建一个Java对象,可以使用this关键字来调用相应的构造器。

        public class Apple
        {
            public String name;
            public String color;
            public double weight;
            public Apple()
            {
            }
            //两个参数的构造器
            public Apple(String name , String color)
            {
                  this.name=name;
                  this.color=color;
            }
            //三个参数的构造器
            public Apple(String name , String color , double weight)
            {
                  //通过this调用另一个重载的构造器的初始化代码
                  this(name, color);
                  //下面this引用该构造器正在初始化的Java对象
                  this.weight=weight;
            }
        }

使用this调用另一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句。使用this调用重载的构造器时,系统会根据this后括号里的实参来调用形参列表与之对应的构造器。

为什么要用this来调用另一个重载的构造器?我把另一个构造器里的代码复制、粘贴到这个构造器里不就可以了吗?

在软件开发里有一个规则:不要把相同的代码段书写两次以上!因为软件是一个需要不断更新的产品,如果有一天需要更新图5.16中构造器A的初始化代码,假设构造器B、构造器C……里都包含了相同的初始化代码,则需要同时打开构造器A、构造器B、构造器C……的代码进行修改;反之,如果构造器B、构造器C……是通过this调用了构造器A的初始化代码,则只需要打开构造器A进行修改即可。因此,尽量避免相同的代码重复出现,充分复用每一段代码,既可以让程序代码更加简洁,也可以降低软件的维护成本。

3 类的继承

3.1 继承的特点

继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Java的继承具有单继承的特点,每个子类只有一个直接父类。

子类扩展了父类,将可以获得父类的全部Field和方法,这与汉语中的继承(子辈从父辈那里获得一笔财富称为继承)具有很好的类似性。值得指出的是,Java的子类不能获得父类的构造器。

3.2 重写父类的方法

这种子类包含与父类同名方法的现象被称为方法重写,也被称为方法覆盖(Override)。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。

方法的重写要遵循“两同两小一大”规则,“两同”即方法名相同、形参列表相同;“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。

例如,如下代码将会引发编译错误。

        class BaseClass
        {
            public static void test(){...}
        }
        class SubClass extends BaseClass
        {
            public void test(){...}
        }

当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。如果需要在子类方法中调用父类中被覆盖的方法,则可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法。

如果父类方法具有private访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。例如,下面代码是完全正确的。

        class BaseClass
        {
            //test方法是private访问权限,子类不可访问该方法
            private void test(){...}
        }
        class SubClass extends BaseClass
        {
            //此处并不是方法重写,所以可以增加static关键字
            public static void test(){...}
        }

3.3 super限定

如果在某个方法中访问名为a的Field,但没有显式指定调用者,则系统查找a的顺序为:
(1)查找该方法中是否有名为a的局部变量;
(2)查找当前类中是否包含名为a的Field;
(3)查找a的直接父类中是否包含名为a的Field,依次上溯a的所有父类,直到java.lang.Object类,如果最终不能找到名为a的Field,则系统出现编译错误。

当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为它从父类继承得到的所有实例变量分配内存,即使子类定义了与父类中同名的实例变量。也就是说,当系统创建一个Java对象时,如果该Java类有两个父类(一个直接父类A,一个间接父类B),假设A类中定义了2个实例变量,B类中定义了3个实例变量,当前类中定义了2个实例变量,那么这个Java对象将会保存2+3+2个实例变量。

如果在子类里定义了与父类中已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量。注意不是完全覆盖,因此系统在创建子类对象时,依然会为父类中定义的、被隐藏的变量分配内存空间。

3.4 调用父类构造器

子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始化代码,类似于前面所介绍的一个构造器调用另一个重载的构造器。

在一个构造器中调用另一个重载的构造器使用this调用来完成,在子类构造器中调用父类构造器使用super调用来完成。

如果被覆盖的是类Field,在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类Field。

使用super调用父类构造器也必须出现在子类构造器执行体的第一行,所以this调用和super调用不会同时出现

不管我们是否使用super调用来执行父类构造器的初始化代码,子类构造器总会调用父类构造器一次。子类构造器调用父类构造器分如下几种情况。

子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器。

子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中另一个构造器时即会调用父类构造器

子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器

执行父类构造器时,系统会再次上溯执行其父类构造器……依此类推,创建任何Java对象,最先执行的总是java.lang.Object类的构造器。

4 多态

Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态(Polymorphism)。

4.1 多态性

        class BaseClass
        {
            public int book=6;
            public void base()
            {
                  System.out.println("父类的普通方法");
            }
            public void test()
            {
                  System.out.println("父类的被覆盖的方法");
            }
        }
        public class SubClass extends BaseClass
        {
            //重新定义一个book实例Field隐藏父类的book实例Field
            public String book="轻量级Java EE企业应用实战";
            public void test()
            {
                  System.out.println("子类的覆盖父类的方法");
            }
            public void sub()
            {
                  System.out.println("子类的普通方法");
            }
            public static void main(String[] args)
            {
                  //下面编译时类型和运行时类型完全一样,因此不存在多态
                  BaseClass bc=new BaseClass();
                  //输出 6
                  System.out.println(bc.book);
                  //下面两次调用将执行BaseClass的方法
                  bc.base();
                  bc.test();
                  //下面编译时类型和运行时类型完全一样,因此不存在多态
                  SubClass sc=new SubClass();
                  //输出"轻量级J2EE企业应用实战"
                  System.out.println(sc.book);
                  //下面调用将执行从父类继承到的base方法
                  sc.base();
                  //下面调用将执行当前类的test方法
                  sc.test();
                  //下面编译时类型和运行时类型不一样,多态发生
                  BaseClass ploymophicBc=new SubClass();
                  //输出 6 —— 表明访问的是父类Field
                  System.out.println(ploymophicBc.book);
                  //下面调用将执行从父类继承到的base方法
                  ploymophicBc.base();
                  //下面调用将执行当前类的test方法
                  ploymophicBc.test();
                  //因为ploymophicBc的编译时类型是BaseClass
                  //BaseClass类没有提供sub方法,所以下面代码编译时会出现错误
                  //ploymophicBc.sub();
            }
        }

但第三个引用变量ploymophicBc则比较特殊,它的编译时类型是BaseClass,而运行时类型是SubClass,当调用该引用变量的test方法(BaseClass类中定义了该方法,子类SubClass覆盖了父类的该方法)时,实际执行的是SubClass类中覆盖后的test方法,这就可能出现多态了。

当把一个子类对象直接赋给父类引用变量时,例如上面的BaseClass ploymophicBc=new SubClass();这个ploymophicBc引用变量的编译时类型是BaseClass,而运行时类型是SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。

上面的main的方法中注释了ploymophicBc.sub();,这行代码会在编译时引发错误。虽然ploymophicBc引用变量实际上确实包含sub()方法(例如,可以通过反射来执行该方法),但因为它的编译时类型为BaseClass,因此编译时无法调用sub()方法。

与方法不同的是,对象的Field则不具备多态性。比如上面的ploymophicBc引用变量,程序中输出它的book Field时,并不是输出SubClass类里定义的实例Field,而是输出BaseClass类的实例Field。

注意:
引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。因此,编写Java代码时,引用变量只能调用声明该变量时所用类里包含的方法。例如,通过Object p=new Person()代码定义一个变量p,则这个p只能调用Object类的方法,而不能调用Person类里定义的方法。

4.2 引用变量的强制类型转换

编写Java程序时,引用变量只能调用它编译时类型的方法,而不能调用它运行时类型的方法,即使它实际所引用的对象确实包含该方法。如果需要让这个引用变量调用它运行时类型的方法,则必须把它强制类型转换成运行时类型,强制类型转换需要借助于类型转换运算符。

引用类型之间的转换只能在具有继承关系的两个类型之间进行,如果是两个没有任何继承关系的类型,则无法进行类型转换,否则编译时就会出现错误如果试图把一个父类实例转换成子类类型,则这个对象必须实际上是子类实例才行(即编译时类型为父类类型,而运行时类型是子类类型)

考虑到进行强制类型转换时可能出现异常,因此进行类型转换之前应先通过instanceof运算符来判断是否可以成功转换。

        if (objPri instanceof String)
        {
            String str=(String)objPri;
        }

4.3 instanceof运算符

instanceof运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个(也可以是接口,可以把接口理解成一种特殊的类),它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是,则返回true,否则返回false。
instanceof运算符的作用是:在进行强制类型转换之前,首先判断前一个对象是否是后一个类的实例,是否可以成功转换,从而保证代码更加健壮。
instanceof(type)是Java提供的两个相关的运算符,通常先用instanceof判断一个对象是否可以强制类型转换,然后再使用(type)运算符进行强制类型转换,从而保证程序不会出现错误。

5 继承与组合

继承是实现类重用的重要手段,但继承带来了一个最大的坏处:破坏封装。相比之下,组合也是实现类重用的重要方式,而采用组合方式来实现类重用则能提供更好的封装性。

5.1 使用继承的注意点

子类扩展父类时,子类可以从父类继承得到Field和方法,如果访问权限允许,子类可以直接访问父类的Field和方法,相当于子类可以直接复用父类的Field和方法,确实非常方便。

继承带来了高度复用的同时,也带来了一个严重的问题:继承严重地破坏了父类的封装性。前面介绍封装时提到:每个类都应该封装它内部信息和实现细节,而只暴露必要的方法给其他类使用。但在继承关系中,子类可以直接访问父类的Field(内部信息)和方法,从而造成子类和父类的严重耦合。

为了保证父类有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则。

  • 尽量隐藏父类的内部数据。尽量把父类的所有Field都设置成private访问类型,不要让子类直接访问父类的Field。
  • 不要让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private访问控制符修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,则必须以public修饰,但又不希望子类重写该方法,可以使用final修饰符(该修饰符后面会有更详细的介绍)来修饰该方法;如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,则可以使用protected来修饰该方法。
  • 尽量不要在父类构造器中调用将要被子类重写的方法
        class Base
        {
            public Base()
            {
                  test();
            }
            public void test()          //①号test方法
            {
                  System.out.println("将被子类重写的方法");
            }
        }
        public class Sub extends Base
        {
            private String name;
            public void test()       //②号test方法
            {
                  System.out.println("子类重写父类的方法,"
                        + "其name字符串长度" + name.length());
            }
            public static void main(String[] args)
            {
                  //下面代码会引发空指针异常
                  Sub s=new Sub();
            }
        }

当系统试图创建Sub对象时,同样会先执行其父类构造器,如果父类构造器调用了被其子类重写的方法,则变成调用被子类重写后的方法。当创建Sub对象时,会先执行Base类中的Base构造器,而Base构造器中调用了test方法——并不是调用①号test方法,而是调用②号test方法,此时Sub对象的name Field是null,因此将引发空指针异常。

到底何时需要从父类派生新的子类呢?不仅需要保证子类是一种特殊的父类,而且需要具备以下两个条件之一。

  • 子类需要额外增加属性,而不仅仅是属性值的改变。例如从Person类派生出Student子类,Person类里没有提供grade(年级)属性,而Student类需要grade属性来保存Student对象就读的年级,这种父类到子类的派生,就符合Java继承的前提。
  • 子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)。例如从Person类派生出Teacher类,其中Teacher类需要增加一个teaching方法,该方法用于描述Teacher对象独有的行为方式:教学。

5.2 利用组合实现复用

对于继承而言,子类可以直接获得父类的public方法,程序使用子类时,将可以直接访问该子类从父类那里继承到的方法;而组合则是把旧类对象作为新类的Field嵌入,用以实现新类的功能,用户看到的是新类的方法,而不能看到被嵌入对象的方法。因此,通常需要在新类里使用private修饰被嵌入的旧类对象

使用组合关系来实现复用时,需要创建两个Animal对象,是不是意味着使用组合关系系统开销更大?

答:不会。回忆前面介绍继承时所讲的内容,当创建一个子类对象时,系统不仅需要为该子类定义的Field分配内存空间,而且需要为它的父类所定义的Field分配内存空间。如果采用继承的设计方式,假设父类定义了2个Field,子类定义了3个Field,当创建子类实例时,系统需要为子类实例分配5块内存空间;如果采用组合的设计方式,先创建被嵌入类实例,此时需要分配 2 块内存空间,再创建整体类实例,也需要分配 3 块内存空间,只是需要多一个引用变量来引用被嵌入的对象。通过这个分析来看,继承设计与组合设计的系统开销不会有本质的差别。

到底该用继承?还是该用组合呢?继承是对已有的类做一番改造,以此获得一个特殊的版本。简而言之,就是将一个较为抽象的类改造成能适用于某些特定需求的类。因此,对于上面的Wolf和Animal的关系,使用继承更能表达其现实意义。用一个动物来合成一匹狼毫无意义:狼并不是由动物组成的。反之,如果两个类之间有明确的整体、部分的关系,例如Person类需要复用Arm类的方法(Person对象由Arm对象组合而成),此时就应该采用组合关系来实现复用,把Arm作为Person类的嵌入Field,借助于Arm的方法来实现Person的方法,这是一个不错的选择。

总之,继承要表达的是一种“是(is-a)”的关系,而组合表达的是“有(has-a)”的关系。

6 初始化块

与构造器作用非常类似的是初始化块,它也可以对Java对象进行初始化操作。

6.1 使用初始化块

        [修饰符] {
            /   始化块的可执行性代码
            ...
        }

初始化块的修饰符只能是static,使用static修饰的初始化块被称为静态初始化块

从运行结果可以看出,当创建Java对象时,系统总是先调用该类里定义的初始化块,如果一个类里定义了2个普通初始化块,则前面定义的初始化块先执行,后面定义的初始化块后执行。

初始化块只在创建Java对象时隐式执行,而且在执行构造器之前执行

普通初始化块、声明实例Field指定的默认值都可认为是对象的初始化代码,它们的执行顺序与源程序中的排列顺序相同。

        public class InstanceInitTest
        {
            //先执行初始化块将a Field赋值为6
            {
                  a=6;
            }
            //再执行将a Field赋值为9
            int a=9;
            public static void main(String[] args)
            {
                  //下面代码将输出9
                  System.out.println(new InstanceInitTest().a);
            }
        }

提示:
当Java创建一个对象时,系统先为该对象的所有实例Field分配内存(前提是该类已经被加载过了),接着程序开始对这些实例变量执行初始化,其初始化顺序是:先执行初始化块或声明Field时指定的初始值,再执行构造器里指定的初始值。

6.2 初始化块和构造器

在这里插入图片描述

从图中可以看出,如果两个构造器中有相同的初始化代码,这些初始化代码无须接收参数,就可以把它们放在初始化块中定义。通过把多个构造器中的相同代码提取到初始化块中定义,能更好地提高初始化代码的复用,提高整个应用的可维护性。

与构造器类似,创建一个Java对象时,不仅会执行该类的普通初始化块和构造器,而且系统会一直上溯到java.lang.Object类,先执行java.lang.Object类的初始化块,开始执行java.lang.Object的构造器,依次向下执行其父类的初始化块,开始执行其父类的构造器……最后才执行该类的初始化块和构造器,返回该类的对象。

6.3 静态初始化块

如果定义初始化块时使用了static修饰符,则这个初始化块就变成了静态初始化块,也被称为类初始化块。静态初始化块是类相关的,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。因此静态初始化块总是比普通初始化块先执行

静态初始化块不能对实例Field进行初始化处理。

静态初始化块也被称为类初始化块,也属于类的静态成员,同样需要遵循静态成员不能访问非静态成员的规则,因此静态初始化块不能访问非静态成员,包括不能访问实例Field和实例方法。

        class Root
        {
            static{
                  System.out.println("Root的静态初始化块");
            }
            {
                  System.out.println("Root的普通初始化块");
            }
            public Root()
            {
                  System.out.println("Root的无参数的构造器");
            }
        }
        class Mid extends Root
        {
            static{
                  System.out.println("Mid的静态初始化块");
            }
            {
                  System.out.println("Mid的普通初始化块");
            }
            public Mid()
            {
                  System.out.println("Mid的无参数的构造器");
            }
            public Mid(String msg)
            {
                  //通过this调用同一类中重载的构造器
                  this();
                  System.out.println("Mid的带参数构造器,其参数值:"
                        + msg);
            }
        }
        class Leaf extends Mid
        {
            static{
                  System.out.println("Leaf的静态初始化块");
            }
            {
                  System.out.println("Leaf的普通初始化块");
            }
            public Leaf()
            {
                  //通过super调用父类中有一个字符串参数的构造器
                  super("疯狂Java讲义");
                  System.out.println("执行Leaf的构造器");
            }
        }
        public class Test
        {
            public static void main(String[] args)
            {
                  new Leaf();
                  new Leaf();
            }
        }

在这里插入图片描述
一旦Leaf类初始化成功后,Leaf类在该虚拟机里将一直存在,因此当第二次创建Leaf实例时无须再次对Leaf类进行初始化。

注意:
Java系统加载并初始化某个类时,总是保证该类的所有父类(包括直接父类和间接父类)全部加载并初始化

静态初始化块和声明静态Field时所指定的初始值都是该类的初始化代码,它们的执行顺序与源程序中的排列顺序相同

提示:当JVM第一次主动使用某个类时,系统会在类准备阶段为该类的所有静态Field分配内存;在初始化阶段则负责初始化这些静态Field,初始化静态Field就是执行类初始化代码或者声明类Field时指定的初始值,它们的执行顺序与源代码中的排列顺序相同。

本文含有隐藏内容,请 开通VIP 后查看