Java

#back-end/Java/基本语法

封装

封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流和交互。也就是说用户是无需知道对象内部的细节,但可以通过该对象对外的提供的接口来访问该对象。

对于封装而言,一个对象它所封装的是自己的属性和方法,所以它是不需要依赖其他对象就可以完成自己的操作。使用封装有四大好处:

  • 良好的封装能够减少耦合。

  • 类内部的结构可以自由修改。

  • 可以对成员进行更精确的控制。

  • 隐藏信息,实现细节。

    封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果不想被外界方法,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。

以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。
注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。

public class Person {

private String name;
private int gender;
private int age;

public String getName() {
return name;
}

public String getGender() {
return gender == 0 ? "man" : "woman";
}

public void work() {
if (18 <= age && age <= 50) {
System.out.println(name + " is working very hard!");
} else {
System.out.println(name + " can't work any more!");
}
}
}

继承

继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。
继承所描述的是“IS-A”的关系,如果有两个对象 A 和 B ,若可以描述为“A是B”,则可以表示 A 继承 B ,其中B是被继承者称之为父类或者超类, A 是继承者称之为子类或者派生类。
实际上继承者是被继承者的特殊化,它除了拥有被继承者的特性外,还拥有自己独有得特性。例如猫有抓老鼠、爬树等其他动物没有的特性。同时在继承关系中,继承者完全可以替换被继承者,反之则不可以,例如我们可以说猫是动物,但不能说动物是猫就是这个道理,其实对于这个我们将其称之为“向上转型”。
诚然,继承定义了类如何相互关联,共享特性。对于若干个相同或者相识的类,我们可以抽象出他们共有的行为或者属相并将其定义成一个父类或者超类,然后用这些类继承该父类,他们不仅可以拥有父类的属性、方法还可以定义自己独有的属性或者方法。

同时在使用继承时需要记住三句话:

  • 子类拥有父类非 private 的属性和方法。
  • 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。

在下面这个示例中,我们定义了两个静态内部类:Animal和Dog。Animal类中有一个方法eat(),用于输出“动物吃食物”的字符串。Dog类继承了Animal类,并新增了一个方法bark(),用于输出“狗叫:汪汪汪”的字符串。

public class Main {
public static class Animal {
public void eat() {
System.out.println("动物吃食物");
}
}

public static class Dog extends Animal {
public void bark() {
System.out.println("狗叫:汪汪汪");
}
}

public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // 调用继承自父类Animal的方法
dog.bark(); // 调用子类Dog的方法
}
}

构造器

通过前面我们知道子类可以继承父类的属性和方法,除了那些 private 的外还有一样是子类继承不了的—构造器。对于构造器而言,它只能够被调用,而不能被继承。 调用父类的构造方法我们使用 super() 即可。
构建过程是从父类“向外”扩散的,也就是从父类开始向子类一级一级地完成构建。而且我们并没有显示的引用父类的构造器,这就是 java 的聪明之处:编译器会默认给子类调用父类的构造器。
但是,这个默认调用父类的构造器是有前提的:父类有默认构造器。如果父类没有默认构造器,我们就要必须显示的使用 super() 来调用父类构造器,否则编译器会报错:无法找到符合父类形式的构造器。
对于子类而已,其构造器的正确初始化是非常重要的,而且当且仅当只有一个方法可以保证这点:在构造器中调用父类构造器来完成初始化,而父类构造器具有执行父类初始化所需要的所有知识和能力。

对于继承而言,子类会默认调用父类的构造器,但是如果没有默认的父类构造器,子类必须要显示的指定父类的构造器,而且必须是在子类构造器中做的第一件事(第一行代码)。

super关键字调用父类构造方法

在Java中,每个类都有一个构造方法,用于创建这个类的对象。构造方法的主要作用是初始化对象的属性,确保对象能够正常工作。在创建对象时,构造方法会被自动调用。

在继承关系中,子类会自动继承父类的构造方法,但是构造方法不能被继承,只能被调用。因此,在子类中必须显式调用父类的构造方法,以确保父类的属性也能被正确地初始化。这时我们可以使用super关键字来调用父类的构造方法。

使用super关键字调用父类构造方法的格式如下所示:
super(参数列表);
这里的参数列表指的是父类构造方法的参数列表,包括类型和参数名。父类构造方法会根据传入的参数初始化其属性。

示例代码及其分析

现在我们来看下面这个示例代码:

public class Main {
public static class Parent {
private final int age;

public Parent(int age) {
this.age = age;
}

public int getAge() {
return age;
}
}

public static class Child extends Parent {
private final String name;

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

public String getName() {
return name;
}
}

public static void main(String[] args) {
Child child = new Child(10, "Tom");
System.out.println("child's age: " + child.getAge()); // 输出:child's age: 10
System.out.println("child's name: " + child.getName()); // 输出:child's name: Tom
}
}

在Parent类中定义了一个私有属性age,并提供了一个有参构造方法Parent(int age)用于初始化这个属性。在Child类中,我们继承了Parent类,并定义了一个私有属性name,以及一个有参构造方法Child(int age, String name)用于初始化age和name属性。在Child的构造方法中,我们调用了父类Parent的构造方法super(age),将传入构造方法的age参数传递给父类,以初始化父类的属性。然后再初始化子类Child的属性name。

在Main类的main方法中,我们创建了一个Child对象child,并分别调用了child的getAge方法和getName方法,输出了它的属性值。最终输出结果为:child’s age: 10和child’s name: Tom。

通过这个例子,我们可以看到,通过使用super关键字,子类Child能够调用父类Parent的构造方法,来初始化父类的属性。这样,我们就能够实现在子类中初始化多重属性,同时也确保了父类中的属性也能被正确地初始化。

protected关键字

private 访问修饰符,对于封装而言,是最好的选择,但这个只是基于理想的世界,有时候我们需要这样的需求:我们需要将某些事物尽可能地对这个世界隐藏,但是仍然允许子类的成员来访问它们。这个时候就需要使用到 protected。
对于 protected 而言,它指明就类用户而言,他是 private,但是对于任何继承与此类的子类而言或者其他任何位于同一个包的类而言,他却是可以访问的。

示例代码及其分析

// Vehicle.java
class Vehicle {
private int speed;

protected void run() {
System.out.println("Vehicle is running at " + getSpeed() + " km/h");
}

protected void setSpeed(int speed){
this.speed = speed;
}

protected int getSpeed(){
return speed;
}
}

// Car.java
class Car extends Vehicle {
public Car(int speed) {
setSpeed(speed);
}
}

// Main.java
public class Main {
public static void main(String[] args) {
Car car = new Car(120);
car.run(); // 输出:Vehicle is running at 120 km/h
}
}

关于作用:

  • protected访问修饰符允许其子类访问其定义的成员(方法和变量),并且允许同一个包内的其他类访问这些成员;
  • private访问修饰符用来限定某个成员只能在定义该成员的类内部被访问。

在这个例子中,将run()方法和speed成员变量定义为protected和private访问修饰符,可以确保speed成员变量只有在Vehicle类及其子类(例如Car类)中被访问和修改,保证了程序的安全性。

向上转型

在上面的继承中我们谈到继承是 IS-A 的相互关系,猫继承与动物,所以我们可以说猫是动物,或者说猫是动物的一种。这样将猫看做动物就是向上转型。
Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为向上转型

Animal animal = new Cat();

向上转型是指将一个子类对象赋值给一个父类引用变量的过程,可以将子类对象当做父类对象对待。这个过程可以自动完成,因为子类对象包含了父类对象的全部特性,所以它可以被当作父类对象使用。在向上转型中,虽然父类引用变量只能看到从父类继承而来的属性和方法,但是实际执行的方法却是子类的方法,并且还能够访问子类中扩展的属性和方法。以下是一个例子:

class Animal {
public void move() {
System.out.println("Animal is moving!");
}
}

class Cat extends Animal {
@Override
public void move() {
System.out.println("Cat is moving!");
}

public void jump() {
System.out.println("Cat is jumping!");
}
}

public class Main {
public static void main(String[] args) {
Animal animal = new Cat(); // 向上转型
animal.move(); // 执行的是 Cat 类中的 move() 方法
//animal.jump(); // 编译错误,Animal 类型没有 jump() 方法

Cat cat = new Cat();
cat.move(); // 执行的是 Cat 类中的 move() 方法
cat.jump(); // 执行 Cat 类中的 jump() 方法
}
}

在上面的例子中,Cat 类继承自 Animal 类,并覆盖了 Animal 类中的 move() 方法,同时还新增了 jump() 方法。在 main() 方法中,Animal 类型的引用变量 animal 指向了 Cat 类型的对象,这就是向上转型,此时调用 move() 方法实际执行的是 Cat 类中的 move() 方法,因为 Cat 类覆盖了父类的 move() 方法。但是如果尝试调用 jump() 方法,就会导致编译错误,因为 jump() 方法是子类新增的方法,而 Animal 类没有实现该方法。在这个例子中,我们展示了向上转型的用法,即将子类对象当做父类对象对待,以实现代码的灵活使用和复用。

在向上转型的过程中,父类引用变量只能看到从父类继承而来的属性和方法,而子类中新增的方法和属性是无法被父类引用变量访问的。但是,如果子类覆盖了从父类继承而来的方法,并且在子类中重新实现了该方法,那么在父类引用变量调用该方法的时候,实际执行的是子类中重新实现的方法。这个过程中并没有修改父类的方法,而是在子类中重新实现了该方法,称为方法的覆盖。

谨慎继承

在这里我们需要明确,继承存在如下缺陷:

  • 父类变,子类就必须变。
  • 继承破坏了封装,对于父类而言,它的实现细节对与子类来说都是透明的。
  • 继承是一种强耦合关系。

所以说当我们使用继承的时候,我们需要确信使用继承确实是有效可行的办法。那么到底要不要使用继承呢?《Think in java》 中提供了解决办法:问一问自己是否需要从子类向父类进行向上转型。如果必须向上转型,则继承是必要的,但是如果不需要,则应当好好考虑自己是否需要继承。

多态

所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

多态分为编译时多态运行时多态:

  • 编译时多态主要指方法的重载
  • 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定

所以对于多态我们可以总结如下:
指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的,尽管是重载该方法。若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。

对于面向对象而言,多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。

多态的实现条件

在刚刚开始就提到了继承在为多态的实现做了准备。子类 Child 继承父类 Father,我们可以编写一个指向子类的父类类型引用,该引用既可以处理父类 Father 对象,也可以处理子类 Child 对象,当相同的消息发送给子类或者父类对象时,该对象就会根据自己所属的引用而执行不同的行为,这就是多态。即多态性就是相同的消息使得不同的类做出不同的响应。

Java实现多态有三个必要条件:继承重写向上转型

  • 继承:在多态中必须存在有继承关系的子类和父类。

  • 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。

  • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

    只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。

    对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。

下面的代码中,乐器类 (Instrument) 有两个子类: Wind 和 Percussion,它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。

public class Instrument {
public void play() {
System.out.println("Instrument is playing...");
}
}

public class Wind extends Instrument {
public void play() {
System.out.println("Wind is playing...");
}
}

public class Percussion extends Instrument {
public void play() {
System.out.println("Percussion is playing...");
}
}

public class Music {
public static void main(String[] args) {
List<Instrument> instruments = new ArrayList<>();
instruments.add(new Wind());
instruments.add(new Percussion());
for(Instrument instrument : instruments) {
instrument.play();
}
}
}

参考资料

面向对象编程三大特性——封装、继承、多态
Java 基础 - 面向对象