Java——面向对象(三) 封装、继承、多态

隐藏与封装


前面程序中经常出现通过某个对象直接访问成员变量的情形,这可能引起一些潜在的问题,比如将一个人的年龄设置为1000。显然这是违背现实的,因此java推荐将类和对象的成员变量进行封装。

什么是封装?

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

封装的好处

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

实际上封装的两个含义是:把该隐藏的隐藏起来,把该暴露的暴露出来。这两个方面都可以通过java提供访问控制符来实现。

访问控制符
java提供了三个访问控制符:private、protected、public,另外还有不加控制符的访问控制级别。
级别排序:

private>default>protected>public
级别由小到大

控制符 描述
private(当前类访问权限) 被修饰的成员只能在当前类内部被访问
default(包访问权限) 默认不加控制符,就为default,被修饰的成员或外部类可以被相同包下的其他类访问
protected(子类访问权限) 被修饰的成员即可以被同一包的其他类访问,也可以被不同包的其他子类访问
public(公共访问权限) 被修饰的成员可以被所有类访问

实例

隐藏属性

只能在类的内部使用,而对象不能直接使用。

 //成员变量
    //关键字private表明这个属性是私有属性
    private String name="M"; //给出默认值
    private Double age;
    private String color;

可以看到在测试类的时候,这些属性都不能通过对象直接调用;

Java——面向对象(三) 封装、继承、多态

但是可以通过设定设置私有属性的方法和获取私有属性的方法来使用私有属性;

 //设置私有属性Name
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

通过快捷键来定以方法set和get;

点击右键

Java——面向对象(三) 封装、继承、多态

Java——面向对象(三) 封装、继承、多态
Java——面向对象(三) 封装、继承、多态
这样可以快捷的定义方法。

使用私有属性

在类的外边使用私有属性,通过刚才定义的方法:

     //设置私有属性
        dog.setName("WangWang");

        //获取私有属性
        String name=dog.getName();
//        WangWang

//        调用成员方法
        dog.eat(name);
        dog.sleep(name);
        
        /*the WangWang dog is eating
        the WangWang dog is sleeping*/

关于访问控制符的使用,存在如下几条基本原则:

  • 类里面绝大部分成员变量都应该使用private修饰,只有一些static修饰的,类似于全局变量的成员变量,才可以考虑使用public修饰。除此之外,有些方法只用于辅助实现该类的其他方法,被称为工具方法,这种也可以用private修饰;
  • 如果某个类主要用做其他类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不想被外界调用,则应该适用protected修饰这些方法。
  • 大部分外部类都是用public修饰。

代码块


什么是代码块?
在Java中,使用{}括起来的代码被称为代码块。

代码块分类
根据其位置和声明的不同可以分为:

  • 局部代码块:在方法中出现,限定变量生命周期,随着方法的调用而调用,销毁而销毁,及早释放,提高内存利用率。

public class CodeStock {
    public static void main(String[] args) {
        int num=100;
        {
            System.out.println(num);
            num=10;
        }
        System.out.println(num);
    } }

  • 构造代码块:在类中方法外出现,多个构造方法方法中相同的代码存放到一起,每次调用构造都执行,并且在构造方法前执行。
package org.westos.practice;

public class Student {
    String name;

    public Student(){
        System.out.println("我是构造方法");
    }

    {
        System.out.println("我是构造代码块");
    }
    
    public static void main(String[] args) {
        Student student = new Student();
    }
}

/*我是构造代码块
我是构造方法*/

  • 静态代码块:在类中方法外出现,加static修饰,用于给类进行初始化,在加载类的时候执行一次。
package org.westos.practice;

public class Student {
    String name;

    public Student(){
        System.out.println("我是构造方法");
    }

    {
        System.out.println("我是构造代码块");
    }

    static {
        System.out.println("我是静态代码块");
    }

    public static void main(String[] args) {
        Student student1 = new Student();
        Student student2 = new Student();
    }
}

/*我是静态代码块
我是构造代码块
我是构造方法
我是构造代码块
我是构造方法*/

继承


什么是继承?
继承是面向对象的三大特征之一,多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。

如何实现继承?
通过extends关键字可以实现类的继承;

格式:

class 子类名 extends 父类名{}

实现继承的类称为子类,被继承的类称为父类,有的也称为基类、超类。

对继承的理解
父类和子类的关系一般与特殊,抽象与具体,广义和狭义的关系。比如父类为动物类,是一个抽象的概念,子类继承父类所以子类可以是猫类,狗类,老虎类等,是将抽象的动物类具体到哪类动物。
如果想实现多层继承,比如Tom这个猫还要继承猫类,则Tom可以称为动物类的子孙类,动物类称为Tom类的祖先类

实例

Animal.class

package org.westos.practice;

public class Animal {
    
    //给出默认值Tom
    String name=“Tom”;
    int age;
    
    public void eat(){
        System.out.println("动物喜欢吃饭");
    }
    
}

Cat.class

package org.westos.practice;

public class Cat extends Animal {

    @Override
    public void eat() {
        System.out.println("猫喜欢吃鱼");
    }
}

Tom.class

package org.westos.practice;

public class Tom extends Cat {

    @Override
    public void eat() {
        System.out.println(this.name+"喜欢吃带鱼");
    }
}

Test01.class测试类

package org.westos.practice;

public class Test01 {
    public static void main(String[] args) {

        Animal animal = new Animal();
        Cat cat = new Cat();
        Tom tom = new Tom();

        animal.eat();
        cat.eat();
        tom.eat();
    }
}
/*动物喜欢吃饭
猫喜欢吃鱼
Tom喜欢吃带鱼*/

可以从上面的代码中体会到多层继承将类的范围越来越具体到实例。并且在Tom类中用自身的对象可以访问到其祖先类的name属性,而在Tom中并没有定义name属性。
继承的优缺点

  • 优点:提高了代码的复用性;提高了代码的维护性;让类与类之间产生了关系,多态的前提;
  • 缺点:类的耦合性增强了;而开发的原则是高内聚,低耦合,大概意思是尽量降低类与类之间的依赖关系,增强类内部实现事物的能力。

继承的特点

  • java只支持单继承,不支持多继承,换句话说就是子类只能有一个父类而不能继承多个父类;
  • java实现多层继承:比如上述实例中有双层继承,即Cat继承Anaimal,Tom继承Cat,这种就叫做多层继承。

在Animal中没有显式的指明其父类,其实它是有继承父类的,不过是缺>省值。在java中没有显式指明父类的类,其父类默认>为java.lang.Object类,所以可以说Object是所有类的父类,或是直接>父类或是间接父类。

继承的注意事项

  • 子类只能继承父类所有非私有的成员;
  • 子类不能继承父类的构造方法(构造方法不参与继承),但可以通过super关键字去访问父类的构造方法;
  • 不要为了部分功能而去继承;

方法重写
上面的三个继承类中都用到了方法重写,什么是方法重写呢?

子类中出现了和父类中一模一样的方法声明,也可称为方法覆盖,方法复写。

方法重写的应用

当子类需要父类的功能,而功能主体自雷有自己特有的内容时,可以重写父类中的方法,这样沿袭了父类的功能,又定义了子类特有的内容。

对方法重写的理解
如果父类时鸟类,鸟类都包含了飞翔的方法,鸵鸟也属于鸟类,可是将飞翔的方法放在他身上显然不合适,所以鸵鸟类就要重写飞翔这个方法。

方法重写的注意事项

  • 父类中私有的方法不能被重写,因为父类私有方法子类根本就无法继承;
    如将上述实例中的成员方法eat设置为私有成员,在重写就会报错;
    Animal.class
 private void eat(){
        System.out.println("动物喜欢吃饭");
    }

执行测试类

Error:(10, 15) java: eat()可以在org.westos.practice.Animal中访问private
  • 子类重写父类方法时,访问权限不能更低,也就是子类的权限不能比父类的权限级别低,最好权限一致。
    如Cat类中eat方法权限为public,现在将Tom类中的eat方法权限改为比public小的private;
public class Tom extends Cat {

    @Override
    private void eat() {
        System.out.println(this.name+"喜欢吃带鱼");
    }
}
Error:(6, 18) java: org.westos.practice.Tom中的eat()无法覆盖org.westos.practice.Cat中的eat()
  正在尝试分配更低的访问权限; 以前为public

可以看到权限太低不能覆盖父类的方法。

  • 父类静态方法,子类也必须通过静态方法进行重写;

super关键字
当父类中的方法杯子类的方法覆盖,子类的对象将无法访问父类中被覆盖的方法,但是在子类方法中可以调用父类中被覆盖的方法。使用super关键字或者父类类名作为调用者来调用父类中被覆盖的方法。

例如在Tom类的eat方法中调用已经被覆盖的Cat的eat方法;

    @Override
    public void eat() {
        System.out.println(this.name+"喜欢吃带鱼");
        super.eat(); //调用父类被覆盖的方法
    }
}

正如this不能在static修饰的方法中使用一样,super也不能在static修饰的方法中使用,在每个类里面的构造方法的第一行默认都有一行super();去调用父类的空参构成,先要完成父类数据的初始化,然后再初始化自己的数据。


继承中成员变量的关系

  • 子类中的成员变量和父类中的成员变量名称不一样时,直接通过名称调用;
  • 当子类的成员变量与父类中的成员名一样时遵循就近原则,按下面的先后顺序,即在子类的方法局部范围找,在子类的成员范围找;在父类的成员范围找;找不到就报错。

Father.class

package org.westos.practice;

public class Father {
    int num=10;
    int num2=1;
}

Son.class

package org.westos.practice;

public class Son extends Father {
    int num=20;

    public void show(){
        System.out.println(num);
        System.out.println(num2);
    }
}

Test.class

package org.westos.practice;

public class Test02 {


    public static void main(String[] args) {
        Son son = new Son();
        int num=30;
        System.out.println(num);
        son.show();
    }
}
/*30
20
1*/

当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为他从父类继承得到的所有实例变量分配内存,即使子类定义了与父类同名的实例变量。

如果在子类里定义了与父类已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量,而不是覆盖。
Parent.class

package org.westos.practice;

public class Parent {
    public String tag="ParentJava";
}

Derived.class

package org.westos.practice;

public class Derived extends Parent{
        //定义一个私有tag实例变量来隐藏父类中的tag实例变量
        private String tag="DerivedJava";
}

Test03.class

package org.westos.practice;

public class Test03 {
    public static void main(String[] args) {
        Derived derived = new Derived();

        //程序不可访问derived的私有属性atg
        System.out.println(derived.tag);
//        Error:(8, 35) java: tag可以在org.westos.practice.Derived中访问private

//        将derived显式的向上转型为Parent,即可访问tag实例变量
        System.out.println(((Parent)derived).tag);
//        ParentJava

    }
}


调用父类构造器

子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始化代码,类似于前面介绍的构造器调用另一个构造器。
在一个构造器中调用类内部的构造器使用this,调用父类构造器用super。

Base.class

package org.westos.practice;

public class Base {
    public double size;
    public String name;

    public Base(double size, String name) {
        this.size = size;
        this.name = name;
    }
}

Sub.class

package org.westos.practice;

public class Sub extends Base {
    public String color;

    public Sub(double size, String name, String color) {
        //通过super来调用父类构造器的初始化过程
        super(size, name);
        this.color = color;
    }
    public static void main(String[] args) {
        Sub s = new Sub(8,"测试对象","red");

        System.out.println(s.size+"-"+s.name+"-"+s.color);
    }
}

子类构造器调用父类构造器分以下几种情况:

  • 子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器。
  • 子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参列表调用本类的另一个构造器,执行本类的另一个构造器时即会调用父类构造器;
  • 子类构造器执行体中既没有super调用,也没有this调用,系统将会在子类执行构造器之前,隐式调用父类无参数的构造器。

不管上面哪种情况,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行,而且执行父类构造器时系统会再次上溯执行其父类构造器,以此类推直到顶层的Object类的构造器。

Father.class

package org.westos.practice;

public class Father {

    public Father() {
        System.out.println("我是父类构造器");
    }
}

Son.class

package org.westos.practice;

public class Son extends Father{
    public Son() {
        System.out.println("我是子类构造器");
    }

    public static void main(String[] args) {
        Son son = new Son();
    }
}
/*我是父类构造器
我是子类构造器*/

可以看出,创建任何对象总是从该类所在继承数最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。


多态

什么是多态?
Java引用变量有两个类型:

  • 编译时类型:由声明该变量时使用的类型决定;
  • 运行时类型:由实际赋给变量的对象决定;

如果编译时类型和运行时类型不一致,就可出现多态

对多态的理解

某一个事物在不同时刻表现出来的不同状态;

比如:在生物圈来说猫是动物,在动物圈来说猫是猫科动物,在猫科动物里猫是猫类;但是可以说猫是猫科动物,也可以说猫属于动物。

Animal.class

package org.westos.pactice;

public class Animal {
    int num=10;

    public void eat(){
        System.out.println("动物要吃饭");
    }

    public void sleep(){
        System.out.println("动物要睡觉");
    }

    public void show(){
        System.out.println("Animal中的show方法");
    }

    public static void method(){
        System.out.println("Animal中的静态方法");
    }

}

Cat.class

package org.westos.pactice;

public class Cat extends Animal {
    int num=20;

    @Override
    public void eat() {
        System.out.println("猫爱吃鱼");
    }

    @Override
    public void sleep() {
        System.out.println("猫喜欢在白天睡觉");
    }

    @Override
    public void show() {
        System.out.println("Cat中的show方法");
    }
    public static void method(){
        System.out.println("Cat中的静态方法");
    }
}

Test.class

package org.westos.pactice;

public class Test01 {
    public static void main(String[] args) {
        //左右类型一致,不发生多态
        Cat cat = new Cat();
        cat.eat();
        cat.sleep();
        cat.show();
        System.out.println("-------------------");

        //父类引用变量指向子类对象
        //左边为编译时类型,Animal
        //右边为运行时类型,Cat;类型不一致多态发生
        Animal an=new Cat();

        //通过多态访问成员方法
        an.eat();//猫爱吃鱼
        an.sleep();//猫喜欢在白天睡觉
        an.show();//Cat中的show方法
        System.out.println("----------------------");

        //通过太多访问成员变量:编译看左边,运行看右边
        System.out.println("num="+an.num);//num=10
        System.out.println("---------------------");

        //通过多态访问静态方法:编译看左边,运行看左边
        Animal.method();//Animal中的静态方法
        Cat.method();//Cat中的静态方法
        an.method();//Animal中的静态方法

    }
}

从实力可以看出:相同类型的变量调用同一方法时呈现出不同的行为特征,这就是多态

引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。

例如将Cat中的show方法改名为show1,而Animal中没有这个方法。
运行结果

Error:(20, 11) java: 找不到符号
  符号:   方法 show1()
  位置: 类型为org.westos.pactice.Animal的变量 an

实现多态的必要条件

  • 有继承关系
  • 有方法重写
  • 有父类引用指向子类对象

优点:

  • 提高了代码的维护性
  • 提高了代码的扩展性