JavaSE——封装、继承和多态

1. 封装

1.1 概念

     面向对象程序三大特性:封装、继承、多态 。而类和对象阶段,主要研究的就是封装特性。何为封装呢?简单来说就是套壳屏蔽细节
    比如:对于电脑这样一个复杂的设备,提供给用户的就只是:开关机、通过键盘输入,显示器, USB 插孔等,让用户来和计算机进行交互,完成日常事务。但实际上:电脑真正工作的却是CPU 、显卡、内存等一些硬件元件。
   对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的, CPU 内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳 子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可
  封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互

  若进行封装就需要使用访问限定符(稍后详细解释)

1.2 封装的实现

 在代码层面就是把用public 修饰的成员变量/方法改为用private 修饰

代码如下:

class Dog{
    private String name ;
    private int age ;

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void eat(){
        System.out.println(this.name + "正在吃饭!");

    }
    public static void main(String[] args) {
        Dog dog = new Dog("小黄",3);
        System.out.println(dog.name);
    }
}

在同一个包同一个类中,private 修饰的成员变量/方法 可以被直接访问:

除此之外,其他三种情况下都不可以被直接访问:

不过,我们可以通过 get 和 set 方法对private 修饰的成员变量/方法进行间接访问:

class Dog{
    private String name ;
    private int age ;

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void eat(){
        System.out.println(this.name + "正在吃饭!");

    }

}
public class test1 {
    public static void main(String[] args) {
        Dog dog = new Dog("小黄",3);
        System.out.println(dog.getName());
    }
}

运行结果如下:

2. 包

2.1 概念

在面向对象体系中,提出了一个软件包的概念,即: 为了更好的管理类,把多个类收集在一起成为一组,称为软件 。有点类似于目录。比如:为了更好的管理电脑中的歌曲,一种好的方式就是将相同属性的歌曲放在相同文件下,也可以对某个文件夹下的音乐进行更详细的分类。

 

  在 Java 中也引入了包, 包是对类、接口等的封装机制的体现,是一种对类或者接口等的很好的组织方式 ,比如:一个包中的类不想被其他包中的类使用。包还有一个重要的作用:在同一个工程中允许存在相同名称的类,只要处在 不同的包中即可

2.2 如何导入包中的类

1. 直接使用包名导入,例如Date类:可以使用 java.util.Date 导入 java.util 这个包中的 Date类.

不过这个使用有点麻烦。

2. 使用 import语句导入包 

这个就方便多了。如果需要使用 java.util 中的其他类, 可以使用 import java.util.*  

不过这样存在一些问题:容易出现冲突的情况.  

这种情况下,需要使用完整的类名

 所以我们更建议显式的指定要导入的类名.

注意事项 :
   import C++ #include 差别很大 . C++ 必须 #include 来引入其他文件内容 , 但是 Java 不需要 .
import 只是为了写代码的时候更方便 . import 更类似于 C++ namespace using

2.3 自定义包

2.3.1 基本规则

  • 在文件的最上方加上一个 package 语句指定该代码在哪个包中.
  • 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.dadu.www ).
  • 包名要和代码路径相匹配. 例如创建 com.dadu.www 的包, 那么会存在一个对应的路径 com.dadu.www来存储代码.
  • 如果一个类没有 package 语句, 则该类被放到一个默认包中

2.3.2 操作步骤(以ideal为例)

1. IDEA 中先新建一个包 : 右键 src -> 新建 ->
2. 在弹出的对话框中输入包名 , 例如 com.dadu.www
3. 在包中创建类 , 右键包名 -> 新建 -> , 然后输入类名即可 .
4. 此时可以看到我们的磁盘上的目录结构已经被 IDEA 自动创建出来了
5. 同时我们也看到了 , 在新创建的 Test.java 文件的最上方 , 就出现了一个 package 语句

2.4 控制包访问权限举例

1.访问 public 修饰的变量

2. 访问 private 修饰的变量

报错,因为 name 是私有的,不允许其他的类访问。

3. 访问 default 修饰的变量

报错,因为default 修饰的变量不允许被其他包中的类访问。

2.5 其他的包

1. java.lang: 系统常用基础类 (String Object), 此包从 JDK1.1 后自动导入。
2. java.lang.reflect:java 反射编程包 ;
3. java.net: 进行网络编程开发包。
4. java.sql: 进行数据库开发的支持包。
5. java.util: java 提供的工具程序包。 ( 集合类等 ) 非常重要
6. java.io:I/O 编程开发包。

3.继承

3.1 概念

继承 (inheritance) 机制 :是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能 ,这样产生新的类,称 派生类 。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用
例如:狗和猫都是动物,那么我们就可以将共性的内容进行抽取,然后采用继承的思想来达到共用。
   上述图示中, Dog Cat 都继承了 Animal 类,其中: Animal 类称为父类 / 基类或超类 Dog Cat 可以称为 Animal 子类/ 派生类,继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可。
从继承概念中可以看出继承最大的作用就是:实现代码复用,还有就是来实现多态 ( 后序讲 )

3.2 继承的语法

Java中如果要表示类之间的继承关系,需要借助extends关键字,具体如下:

修饰符 class 子类 extends 父类 {
// ...
//...
}
我们将上面图中的示例用代码演示一下:
class Animal {
    public String name;
    public int age;
    public void eat(){
        System.out.println(name + "正在吃饭");
    }
    public void sleep(){
        System.out.println(name + "正在睡觉");
    }
}

class Dog extends Animal{
    public void bark(){
        System.out.println(name +"汪汪叫!");
    }
}
class Cat extends Animal{
    public void mew(){
        System.out.println(name +"喵喵叫!");
    }
}
public class test2{
    public static void main(String[] args) {
        Dog dog = new Dog();
        // dog类中并没有定义任何成员变量,name和age属性肯定是从父类Animal中继承下来的
        System.out.println(dog.name);
        System.out.println(dog.age);
       // dog访问的eat()和sleep()方法也是从Animal中继承下来的
        dog.eat();
        dog.sleep();
        dog.bark();
    }
}

运行结果如下:

注意:
1. 子类会将父类中的成员变量或者成员方法继承到子类中了
2. 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了

3.3 父类成员访问

3.3.1 子类对象访问成员变量

1. 子类和父类不存在同名成员变量

class Data{
     public int a;
     public int b;
}
class Num extends Data{
     public int c;
     public void func(){
         a = 10; // 访问从父类中继承下来的a
         b = 20; // 访问从父类中继承下来的b
         c = 30; // 访问子类自己的c
         
         System.out.println(a + " "+b + " " + c);
     }
}
public class test3 {
    public static void main(String[] args) {
        Num n = new Num();
        n.func();
    }
}
运行结果如下:
2. 子类和父类成员变量同名
class Data{
    public int a = 10;
    public int b = 20;
    public int c = 30;
}
class Num extends Data{
    public int c = 60;
    public void func(){

        System.out.println(a + " "+b + " " + c);
    }
}
public class test3 {
    public static void main(String[] args) {
        Num n = new Num();
        n.func();
    }
}

运行结果如下:

3. 子类和父类都没有同一个变量

通过上述示例我们可知:在子类方法中 或者 通过子类对象访问成员时
  • 如果访问的成员变量子类中有,优先访问自己的成员变量。
  • 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
  • 如果访问的成员变量与父类中成员变量同名,则优先访问自己的。
成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找

3.3.2 子类对象访问成员方法

1. 子类和父类都没有同一个方法
2. 成员方法名字不同
class Data{
   public void func1(){
       System.out.println("Data中的func1方法!");
   }
}
class Num extends Data{

    public void func2(){
        System.out.println("Num中的func2方法!");
    }
    public void func3(){
        func1();
        func2();
    }
}
public class test3 {
    public static void main(String[] args) {
        Num n = new Num();
        n.func3();
    }
}

运行结果如下:

3. 成员方法名字相同
class Data{
    public void func1(){
        System.out.println("Data中的func1方法!");
    }
    public void func2(){
        System.out.println("Data中的func2方法!");
    }
}
class Num extends Data{
    public void func1( int a){
        System.out.println("Num中的func1(int)方法!");
    }
    public void func2(){
        System.out.println("Num中的func2方法!");
    }
    public void func3(){
        func1();        // 没有传参,访问父类中的func1()
        func1(20);   // 传递int参数,访问子类中的func1(int)
        func2();        // 直接访问,则永远访问到的都是子类中的func2(),基类的无法访问到
    }
}
public class test3 {
    public static void main(String[] args) {
        Num n = new Num();
        n.func3();
    }
}

运行结果如下:

通过上述示例我们可知:
  • 通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错。
  • 通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错;

3.4 super 关键字

    由于设计不好,或者因场景需要,子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,该如何操作?直接访问是无法做到的,Java 提供了 super 关键字,该关键字主要作用:在子类方法中访问父 类的成员
具体实现代码如下:
class Data{
    int a;
    int b;
    public void func1(){
        System.out.println("Data中的func1方法!");
    }
    public void func2(){
        System.out.println("Data中的func2方法!");
    }
}
class Num extends Data{
    int a;  // 与父类中成员变量同名且类型相同
    char b; // 与父类中成员变量同名但类型不同
            // 与父类中func1()构成重载
    public void func1( int a){
        System.out.println("Num中的func1(int)方法!");
    }
            // 与父类中func2()构成重写
    public void func2(){
        System.out.println("Num中的func2方法!");
    }
    public void func3(){
        // 对于同名的成员变量,直接访问时,访问的都是子类的
        a = 100; // 等价于: this.a = 100;
        b = 101; // 等价于: this.b = 101;
// 注意:this是当前对象的引用
// 访问父类的成员变量时,需要借助super关键字
// super是获取到子类对象中从基类继承下来的部分
        super.a = 200;
        super.b = 201;
// 父类和子类中构成重载的方法,直接可以通过参数列表区分清访问父类还是子类方法
        func1(); // 没有传参,访问父类中的func1()
        func1(20); // 传递int参数,访问子类中的func1(int)
// 如果在子类中要访问重写的基类方法,则需要借助super关键字
        func2(); // 直接访问,则永远访问到的都是子类中的func2(),基类的无法访问到
        super.func2(); // 访问基类的func2()
    }
}
public class test3 {
    public static void main(String[] args) {
        Num n = new Num();
        n.func3();
    }
}

运行结果如下:

在子类方法中,如果想要明确访问父类中成员时,借助 super 关键字即可。
注意事项:
 1. 只能在非静态方法中使用
 2. 在子类方法中,访问父类的成员变量和方法
super的其他用法在后文中介绍。

3.5 子类构造方法

父子父子,先有父再有子,即:子类对象构造时,需要先调用基类构造方法,然后执行子类的构造方法。
代码示例:
class Data{
    public Data(){
        System.out.println("Data()");
    }
}
class Num extends Data{
    public Num(){
        System.out.println("Num()");
    }

}
public class test3 {
    public static void main(String[] args) {
        Num n = new Num();

    }
}

运行结果如下:

1. 无参构造
子类构造方法中默认会调用基类的无参构造方法:super(),
 用户没有写时,编译器会自动添加,而且super()必须是子类构造方法中第一条语句,并且只能出现一次
示例如下:

2. 有参构造
代码如下:
class Data{
    public int a;
    public int b;
    public Data(int a,int b){
        this.a = a;
        this.b = b;
    }
}
class Num extends Data{
    public int c;
    public Num(int a,int b,int c){
        super(a,b);
        this.c = c;


    }
    public void func(){
        System.out.println(a + " "+b + " " + c);
    }
}
public class test3 {
    public static void main(String[] args) {
        Num n = new Num(10,20,30);
        n.func();
    }
}

运行结果如下:

在子类构造方法中,并没有写任何关于基类构造的代码,但是在构造子类对象时,先执行基类的构造方法,然后执行子类的构造方法,因为:子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分 。父子父子 肯定是先有父再有子,所以在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整 ,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整
注意:
1. 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的 super() 调用,即调用基类构造方法
2. 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。
3. 在子类构造方法中, super(...) 调用父类构造时,必须是子类构造函数中第一条语句。
4. super(...) 只能在子类构造方法中出现一次,并且不能和 this 同时出现

3.6 super 和 this

super this 都可以在成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的第一条语句,那他们之间有什么区别呢?
相同点:
1. 都是 Java 中的关键字
2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点:
1. this 是当前对象的引用,当前对象即调用实例方法的对象, super 相当于是子类对象中从父类继承下来部分成员的引用
2. 在非静态成员方法中, this 用来访问本类的方法和属性, super 用来访问父类继承下来的方法和属性
3. 在构造方法中: this(...) 用于调用本类构造方法, super(...) 用于调用父类构造方法,两种调用不能同时在构造方法中出现
4. 构造方法中一定会存在 super(...) 的调用,用户没有写编译器也会增加,但是 this(...) 用户不写则没有

3.7 继承方式

在现实生活中,事物之间的关系是非常复杂,灵活多样,比如:
但在Java中,只支持几种继承方式:

注意:

一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了. 如果想从语法上进行限制继承, 就可以使用 final 关键字

3.8 final 关键字

final 关键可以用来修饰变量、成员方法以及类。
1. 修饰变量或字段,表示常量 ( 即不能修改 )
示例如下:
2. 修饰类:表示此类不能被继承
3. 修饰方法:表示该方法不能被重写 ( 后序介绍 )

3.9 继承和组合

和继承类似 , 组合也是一种表达类之间关系的方式 , 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字 ), 仅仅是将一个类的实例作为另外一个类的字段。
继承表示对象之间是 is-a 的关系 ,比如:狗是动物,猫是动物
组合表示对象之间是 has-a 的关系 ,比如:汽车

示例如下:

// 轮胎类
class Tire{
// ...
}
// 发动机类
class Engine{
// ...
}
// 车载系统类
class VehicleSystem{
// ...
}
class Car{
    private Tire tire; // 可以复用轮胎中的属性和方法
    private Engine engine; // 可以复用发动机中的属性和方法
    private VehicleSystem vs; // 可以复用车载系统中的属性和方法
// ...
}
// 奔驰是汽车
class Benz extends Car{
// 将汽车中包含的:轮胎、发送机、车载系统全部继承下来
}
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合。

4. 访问限定符

Java 中主要通过类和访问权限来实现封装: 类可以将数据以及封装数据的方法结合在一起 ,更符合人类对事物的认知,而访问权限用来控制方法或者字段能否直接在类外使用 Java 中提供了四种访问限定符:

1. 同一个包的同一类
示例如下:

2. 同一个包不同类

示例如下:

3.不同包中的子类

4. 不同包的非子类 

注意:父类中 private 成员变量虽然在子类中不能直接访问,但是也继承到子类中了
拓展:如何使用访问限定符?
我们希望类要尽量做到 "封装", 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.
因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用 public.
另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是 对访问权限的滥用, 还是更希望同学们能写代码的时候认真思考, 该类提供的字段方法到底给 "谁" 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用)

5. 多态

5.1 概念

多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状 态。

 

总之,同一件事情,发生在不同对象身上,就会产生不同的结果。

5.2 实现条件

java 中要实现多态,必须要满足如下几个条件,缺一不可:
1. 必须在继承体系下
2. 子类必须要对父类中方法进行重写
3. 通过父类的引用调用重写的方法
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
代码示例如下:
class Animal {
    public String name;
    public int age;
    public Animal(String name,int age){
        this.name = name;
        this.age = age;
    }
    public void eat(){
        System.out.println(name + "正在吃饭");
    }
}

class Dog extends Animal{
     public Dog(String name,int age){
         super(name,age);
     }
    @Override
    public void eat(){
        System.out.println(name +"正在吃狗粮!!");
    }
}
class Cat extends Animal{
    public Cat(String name, int age) {
        super(name, age);
    }

    @Override
    public void eat(){
        System.out.println(name +"正在吃猫粮!!");
    }
}
// =======================================================
public class test2{
    public static void main(String[] args) {
        Dog dog = new Dog("小黄",12);
        Cat cat = new Cat("小花",13);
        dog.eat();
        System.out.println("/");
        cat.eat();
    }
}

运行结果如下:

5.3 重写

重写(override) 也称为覆盖。重写是子类对父类非静态、非 private 修饰,非 final 修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变 即外壳不变,核心重写! 重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

5.3.1 规则

  • 子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
  • 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected

  • 父类被staticprivate修饰的方法、构造方法都不能被重写。

  • 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心 将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法 构成重写.

5.3.2 设计原则

对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。
例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来老的类上进行修改,因为原来的 类,可能还在有用户使用 ,正确做法是: 新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我 们当今的需求了

5.3.3 机制

1.静态绑定 :也称为前期绑定 ( 早绑定 ) ,即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
2.动态绑定 :也称为后期绑定 ( 晚绑定 ) ,即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。

5.4 向上转型和向下转型

5.4.1 向上转型

向上转型:实际就是创建一个子类对象,将其当成父类对象来使用,从小范围向大范围的转换。
语法格式:
父类类型  对象名 = new 子类类型 ()

应用场景 :

1. 直接赋值
2. 方法传参
3. 方法返回
示例如下:

向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。

5.4.2 向下转型

将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型。
代码示例如下:
不过还是存在一些问题的,如:要是animal 指向的是狗,但是将animal 还原为猫呢?
所以 向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。 Java 中为了提高向下转型的安全性,引入 instanceof ,如果该表达式为 true ,则可以安全转换。
本文是作者学习后的总结,如果有什么不恰当的地方,欢迎大佬指正!!!
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值