Java面向对象系列[v1.0.0][封装详解]

理解封装

封装(encapsulation)是面向对象的三大特征之一,它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改,而只暴露该暴露的。
比如Person类的成员变量age,只能随着岁月的流逝才会增加,因此age变量是不能随意修改的,对一个类或者对象实现良好的封装:

  • 隐藏类的实现细节
  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制队成员变量的不合理访问
  • 可以进行数据检查,从而有利于保证对象信息的完整性
  • 便于修改,提高代码的可维护性

为了实现良好的封装,需要从两个方面考虑:

  • 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问
  • 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作

访问控制符

Java提供了3个访问控制符,private、protected和public,分别代表了三个访问级别,就控制级别而言,当不加任何访问控制符时被认为是default,控制级别由小到大如图所示
在这里插入图片描述

  • private:【当前类访问权限】如果类里的一个成员(包括变量、方法、构造器等)使用private访问控制符来修饰,这这个成员只能在当前类的内部访问,因此这个访问控制符用于修饰成员变量最合适,使用它来修饰成员变量便可以把成员变量隐藏在类的内部
  • default:【包访问权限】如果类里的一个成员(包括变量、方法、构造器等)或者一个外部类不使用任何访问控制符,则它的访问权限是当前package,也就是说不加任何访问控制符,可以被相同包下的其他类访问
  • protected:【子类访问权限】如果类里的一个成员(包括变量、方法、构造器等)使用protected访问控制符来修饰,那么这个成员既可以被同一个包中的其他类访问,也可以被不同包中的子类访问,通常使用protected来修饰一个方法的时候是为了用于其他子类来重写这个方法的。
  • public:【公共访问权限】如果类里的一个成员(包括变量、方法、构造器等)或者一个外部类用public来修饰,那么这个成员或外部类就可以被所有类访问,无论访问类和被访问类是否在同一个包,是否存在继承关系等等。
    Java面向对象系列[v1.0.0][封装详解]_第1张图片
  • 对于局部变量而言,其作用域就是它所在的方法,不可能被其他类访问,因此不能使用访问控制符来修饰
  • 如果一个java源文件中所有类都不是public来修饰的,则这个java源文件的文件名可以是一切合法的文件名
  • 如果一个java源文件中定义了一个public修饰的类,则这个java源文件的文件名必须与public修饰的类的类名相同
public class Person
{
    // 使用private修饰成员变量,将这些成员变量隐藏起来
    private String name;
    private int age;
    // 提供方法来操作name成员变量
    public void setName(String name)
    {
        // 执行合理性校验,要求用户名必须在2~6位之间
        if (name.length() > 6 || name.length() < 2)
        {
            System.out.println("您设置的人名不符合要求");
            return;
        }
        else
        {
            this.name = name;
        }
    }
    public String getName()
    {
        return this.name;
    }
    // 提供方法来操作age成员变量
    public void setAge(int age)
    {
        // 执行合理性校验,要求用户年龄必须在0~100之间
        if (age > 100 || age < 0)
        {
            System.out.println("您设置的年龄不合法");
            return;
        }
        else
        {
            this.age = age;
        }
    }
    public int getAge()
    {
        return this.age;
    }
}
// 定义了name和age两个成员变量,只有在Person类内才可以操作和访问
// 在Person类之外只能通过各自对应的setter和getter方法来操作和访问他们

Java类里实例变量的setter和getter方法有非常重要的意义,如果java类中每个实例变量都被使用private修饰,并为每个实例变量都提供了public的setter和getter方法,那么这个类就是一个符合JavaBean规范的类

public class PersonTest
{
    public static void main(String[] args)
    {
        var p = new Person();
        // 因为age成员变量已被隐藏,所以下面语句将出现编译错误。
        // p.age = 1000;
        // 下面语句编译不会出现错误,但运行时将提示"您设置的年龄不合法"
        // 程序不会修改p的age成员变量
        p.setAge(1000);
        // 访问p的age成员变量也必须通过其对应的getter方法
        // 因为上面从未成功设置p的age成员变量,故此处输出0
        System.out.println("未能设置age成员变量时:"
            + p.getAge());
        // 成功修改p的age成员变量
        p.setAge(30);
        // 因为上面成功设置了p的age成员变量,故此处输出30
        System.out.println("成功设置age成员变量后:"
            + p.getAge());
        // 不能直接操作p的name成员变量,只能通过其对应的setter方法
        // 因为"李刚"字符串长度满足2~6,所以可以成功设置
        p.setName("李刚");
        System.out.println("成功设置name成员变量后:"
            + p.getName());
    }
}

关于类设计基本规则

  • 一个类常常是一个小模块,应该只让这个模块公开必须让外界知道的内容,隐藏其他一切内容,程序设计时尽量避免一个模块直接操作和访问另一个模块的数据
  • 模块设计追求高内聚:尽可能把模块内部数据、功能实现细节隐藏在模块内部独立完成,不允许外界直接干预
  • 模块设计追求低耦合:仅暴露少量的方法给外部使用

关于访问控制符使用基本规则

  • 类里绝大部分成员变量都应该使用private修饰,只有一些static修饰的、类似全局变量的成员变量,才可以考虑使用public
  • 类里有些方法只用于辅助实现该类的其他方法,这些方法被成为工具方法,也应用private修饰
  • 某个类主要用于做其他类的父类,该类里包含的大部分方法可能仅仅是希望被其子类重写,而不想被外界直接调用,则应该使用protected修饰
  • 希望暴露出来给其他类自由调用的的方法应该使用public修饰,例如类的构造器通过使用public修饰,从而允许在其他地方创建该类的实例,此外大部分外部类也是public修饰,外部类的作用就是被其他类自由使用

使用package和import

package

Java允许将一组功能相关的类放在同一个package下,从而组成逻辑上的类库单元,如果希望把一个类放在指定的包结构下,需要在源文件的第一行非注释行放置代码:package packageName;
一旦在Java源文件中使用了这个package语句,这意味着源文件里定义的所有类都属于这个package,位于包中的的每个类的完整类名都应该是包名和类名的组合,那么如果要是使用该包下的类,那么必须使用包名加类名的组合

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

使用命令javac -d .Hello.java文件,-d选项用于设置编译生成class文件的保存位置,这里指定将生成的class文件放在当前路径下,编译完成后,发下当前路径下并没有Hello.class文件,而是在当前路径下生成了一个名为leadscloud的文件夹,该文件夹下有一个Hello.class文件
如果编译java文件的时候没有使用-d选项,将会在当前路径下直接生成Hello.class文件,而不会生成相应的文件结构,因此当我们编译java文件的时候不建议省略-d选项,即便我们想把生成的class文件放在当前路径下也应该使用-d.选项
执行文件的时候,在leadscloud文件夹所在路径下,使用命令 java leadscloud.Hello,是可以得到正确的输出的

D:\BaiduNetdiskDownload\CrazyJava\codes\05\5.4>java leadscloud.Hello
Hello World!

如果进入到生成的leadscloud路径下,直接使用java Hello命令反而会报异常

D:\BaiduNetdiskDownload\CrazyJava\codes\05\5.4\leadscloud>java Hello
错误: 找不到或无法加载主类 Hello
原因: java.lang.NoClassDefFoundError: leadscloud/Hello (wrong name: Hello)

关于路径和包名

如果仅仅是将生成的class放在某一个路径下,然后将这个目录名改成了包名,就认为这个目录就是java的包,是非常大的错误,有了目录结构不等于有了包名,为Java类添加包必须在源文件中通过package语句指定,单靠目录名是不能指定的,java的包机制需要条件

  • 源文件里使用package语句指定包名
  • class文件必须放在对应的路径下
  • Java语法只要求包名是有效的标识符,但从可读性来说,包名应该全部使用小写字母而且应该由一个或多个有意义的单词连缀而成
  • 为了避免不同公司之家的包名重复,Oracle建议使用公司的域名,例如leadscloud.com倒过来写作为包名的开头部分com.leadscloud
  • package语句必须作为源文件的第一条非注释性语句,一个源文件只能指定一个包,源文件包含多个类则这些类将全部位于该包下
  • 如果没有显示的指定package语句,则处于默认报下
  • 同一个包里的类可以自由访问,如下代码所示,但是包里的子包内的类就需要类的全名
package leadscloud.sub;
public class Apple
{
}
package leadscloud;
public class HelloTest
{
    public static void main(String[] args)
    {
        var h = new Hello();
        // 使用类全名的写法
        var a = new leadscloud.sub.Apple();
    }
}
  • 如果父包中的类需要使用子包中的类,则必须使用子包的全名
  • 调用构造器实例化其他包下的类时,也必须使用全名

import

总是使用类的全名,写起来就非常麻烦,因此java提供了import关键字,import可以像某个Java文件中导入指定包层次下某个类或者全部类,import语句应该出现在package之后,类定义之前
一个Java源文件只能包含一个package语句,但可以包含多个import语句用于导入不同层面的类

import package.subpackage...ClassName;
import leadscloud.sub.Apple;
import package.subpackage...*;  //导入包内全部类,*只能代表类不能代表包

使用了import语句后,便大大减少了实际代码中的前缀

package leadscloud;
import leadscloud.sub.Apple;

public class HelloTest
{
    public static void main(String[] args)
    {
        var h = new Hello();
        // 使用类全名的写法
        var a = new leadscloud.sub.Apple();
        // 如果使用import语句来导入Apple类后,就可以不再使用类全名
        var aa = new Apple();
    }
}

特殊情况

  • java默认为所有源文件导入java.lang包下的所有类,因此我们才能直接使用String、System类,而无须显示的使用import语句
  • 有些时候import语句也无法帮助我们导入类,只能在源文件中使用类全名,例如需要同时使用java.sql包里的类和java.util包里的类,则我们会使用import java.sql.*;import java.util.*;,然而当用到Date类的时候便会报错,因为java.sql.Date和java.util.Date都能够匹配,系统就糊涂了,这种情况下就要使用类的全名来解决
   // 为了让引用更明确,即使使用了import语句,也还是需要使用类全名 java.sql.Date d = new
   java.sql.Date();
  • 在JDK1.5以后,增加静态导入的语法,用于导入指定类的静态成员变量、方法或全部的静态成员变量、方法,语法如下,其中第一个用于导入指定类的单个静态成员变量、方法,第二个用于导入指定类的全部静态成员变量、方法。
   import static package.subpackage...ClassName.fieldName|methodName;
   import static package.subpackage...ClassName.*; //*只能代表静态成员变量或者方法名 
  • 实例
   import static java.lang.System.*; 
   import static java.lang.Math.*;
   
   public class StaticImportTest {
       public static void main(String[] args)
       {
           // out是java.lang.System类的静态成员变量,代表标准输出
           // PI是java.lang.Math类的静态成员变量,表示π常量
           out.println(PI);
           // 直接调用Math类的sqrt静态方法
           out.println(sqrt(256));
       } }
  • Java源文件结构
   package语句 
   import | import static 
   public classDefinition | interfaceDefinition | enumDefinition 
   classDefinition |  interfaceDefinition | enumDefinition

Java常用包

  • java.lang:这个包下的是java的核心类,包含String、Math、System和Thread类等,无需导入,系统自动导入该包内所有类
  • java.util:这个包下的是java的大量工具类/接口和集合框架类/接口,例如Arrays、List、Set等
  • java.net:网络编程相关接口/类
  • java.io:输入/输出编程相关接口/类
  • java.text:格式化相关类
  • java.sql:JDBC数据库编程相关接口/类
  • java.swing:抽象窗口工具集的相关接口/类,用于构建GUI程序

构造器的作用和构造器重载

构造器是个特殊的方法,这个特殊方法用于创建实例时执行初始化,构造器是创建对象的重要途径,java类必须包含一个或者多个构造器方法。

使用构造器进行初始化

构造器最大的作用就是在创建对象时执行初始化,当创建一个对象时,系统为这个对象的实例变量进行默认初始化,这种默认的初始化把所有的基本类型的实例变量设为0(数值型实例变量)或false(布尔型实例变量),把所有的引用类型的实例变量设为null
如果想改变这种默认的初始化,让系统创建对象时就为该对象的实例变量显示指定初始值,就可以通过构造器来实现,否则系统将使用一个无参数的并且执行体为空,不做任何事情,无论如何java类至少包含一个构造器。

public class ConstructorTest
{
    public String name;
    public int count;
    // 提供自定义的构造器,该构造器包含两个参数
    public ConstructorTest(String name, int count)
    {
        // 构造器里的this代表它进行初始化的对象
        // 下面两行代码将传入的2个参数赋给this代表对象的name和count实例变量
        this.name = name;
        this.count = count;
    }
    public static void main(String[] args)
    {
        // 使用自定义的构造器来创建对象
        // 系统将会对该对象执行自定义的初始化
        var tc = new ConstructorTest("疯狂Java讲义", 90000);
        // 输出ConstructorTest对象的name和count两个实例变量
        System.out.println(tc.name);
        System.out.println(tc.count);
    }
}
  • 一旦程序员提供了构造器ConstructorTest(String name, int count),系统就不再提供默认的构造器,因此上面的ConstructorTest类不能再通过 new ContructorTest();代码来创建实例,因为该类不再包含无参数的构造器
  • 如果希望保留无参数的构造器,或者希望有多个初始化过程,则可以为该类提供多个构造器,如果一个类里提供了多个构造器,就形成了构造器重载
  • 因为构造器主要用于被其他方法调用,用于返回该类的实例,因而通常把构造器设置为public,从而允许系统中任意位置的类来创建该类的对象,除非在一些极端情况下设置为protected主要用于被其子类调用,设置为private阻止其他类创建该类的实例

构造器重载

  • 同一个类中有多个构造器,多个构造器的形参列表不同,即被称为构造器重载,构造器重载允许java类里包含多个初始化逻辑,从而允许使用不同的构造器来初始化Java对象
  • 构造器必须与类同名
public class ConstructorOverload
{
    public String name;
    public int count;
    // 提供无参数的构造器
    public ConstructorOverload(){}
    // 提供带两个参数的构造器,
    // 对该构造器返回的对象执行初始化
    public ConstructorOverload(String name, int count)
    {
        this.name = name;
        this.count = count;
    }
    public static void main(String[] args)
    {
        // 通过无参数构造器创建ConstructorOverload对象
        var oc1 = new ConstructorOverload();
        // 通过有参数构造器创建ConstructorOverload对象
        var oc2 = new ConstructorOverload(
            "轻量级Java EE企业应用实战", 300000);
        System.out.println(oc1.name + " " + oc1.count);
        System.out.println(oc2.name + " " + oc2.count);
    }
}
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;
    }
}

你可能感兴趣的:(Java基础即高端)