java三大特性之封装

118 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第19天,点击查看活动详情

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进行修改即可。因此,尽量避免相同的代码重复出现,充分复用每一段代码,既可以让程序代码更加简洁,也可以降低软件的维护成本。