Java讲课笔记10:类的封装

零、本讲学习目标

1、掌握类的封装

2、学会使用方法的重载

3、学会方法的递归

封装是面向对象的三大特征之一,理解并掌握封装对于学习Java面向对象的内容十分重要。本节将从类的封装开始讲解,同时在前面方法定义和调用的基础上来进一步学习方法的重载和递归操作。

一、类的封装

1、为什么需要封装

(1)案例演示

  • 在net.hw.lesson10包里创建Person类
    在这里插入图片描述
package net.hw.lesson10;

/**
 * 功能:Person类
 * 作者:华卫
 * 日期:2020年4月28日
 */
public class Person {
    /**
     * 姓名(default访问权限)
     */
    String name;
    /**
     * 人品(default访问权限)
     */
    int character;

    /**
     * 自我介绍方法
     */
    public void speak() {
        System.out.println("嗨,我叫" + name + ",人品值:" + character);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", character=" + character +
                '}';
    }
}
  • 在net.hw.lesson10包里创建TestPerson类
    在这里插入图片描述
package net.hw.lesson10;

/**
 * 功能:测试Person类
 * 作者:华卫
 * 日期:2020年4月28日
 */

public class TestPerson {
    public static void main(String[] args) {
        // 实例化对象
        Person p = new Person();      
        // 设置对象属性
        p.name = "李晓红";
        p.character = -5;
        // 调用对象方法
        p.speak();
    }
}

运行程序,查看结果:
在这里插入图片描述

(2)案例分析

上述案例将人品赋值为一个负数-5,在语法上不会有任何问题,因此程序可以正常运行,但在现实生活中明显是不合理的,一般而言,人品值应该是某个范围内的正数,比如[1, 10]内的某一个整数值。

(3)解决方案

为了避免出现上述不合理的问题,在设计一个Java类时,应该对成员变量的访问作出一些限定,不允许外界随意访问,这就需要实现类的封装。

2、如何实现封装

(1)封装的定义

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

(2)封装的实现

在定义一个类时,将类中的属性私有化,即使用private关键字来修饰,私有属性只能在它所在类中被访问,如果外界想要访问私有属性,需要提供一些使用public修饰的公有方法,其中包括用于获取属性值的getXXX()方法(简称getter)和设置属性值的setXXX()方法(简称setter)。

(3)案例演示

  • 修改Person类,将name与character属性设置为private属性
    在这里插入图片描述
  • 可以利用IntelliJ IDEA提供的快捷方式帮助我们自动生成访问私有属性的getters和setters:

在这里插入图片描述

说明:按组合键<Alt> + <Ins>弹出Generate菜单,选择Getter and Setter,在弹出的对话框里选中需要生成getter与setter的属性,单击【OK】按钮即可自动生成访问私有属性的公共方法。
在这里插入图片描述

  • 修改setCharacter()方法,保证传入的人品值必须在[1, 10]范围之内
    在这里插入图片描述
    通过这个方式就可以保护私有属性,不允许给私有属性设置不符合要求的数值。

此时,Person类代码如下:

package net.hw.lesson10;

/**
 * 功能:Person类
 * 作者:华卫
 * 日期:2020年4月28日
 */
public class Person {
    /**
     * 姓名(private访问权限)
     */
    private String name;
    /**
     * 人品(private访问权限)
     */
    private int character;

    public String getName() {
        return name;
    }

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

    public int getCharacter() {
        return character;
    }

    public void setCharacter(int character) {
        if (character >= 1 && character <= 10) {
            this.character = character;
        } else {
            System.err.println("温馨提示:人品值必须在[1, 10]范围内!");
        }
    }

    /**
     * 自我介绍方法
     */
    public void speak() {
        System.out.println("嗨,我叫" + name + ",人品值:" + character);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", character=" + character +
                '}';
    }
}
  • 修改TestPerson类,通过setXXX()方法给私有属性赋值
    在这里插入图片描述

  • 运行程序TestPerson,查看结果

在这里插入图片描述
Person对象调用setCharacter(-5),该方法会对参数character进行检查,由于当前传入的值不在[1, 10]范围内,因此会输出“温馨提示:人品值必须在[1, 10]范围内!”的信息。此时,character属性没有赋值,因此保持其初始值0,从而输出“人品值:0”。

  • 如果设置的人品值不符合要求,那么调用自我介绍方法时不输出任何内容。
    在这里插入图片描述

  • 运行TestPerson程序,查看结果
    在这里插入图片描述

  • 修改TestPerson,设置符合要求的人品值,再运行程序,查看效果

在这里插入图片描述

人品值8在[1, 10]范围内,符合要求,属性设置成功,因此输出了“人品值:8”.

3、课堂练习:创建三角形类Triangle

创建三角形类Triangle,包含三个私有属性a、b、c,提供对私有属性的存取方法,对于setXXX()方法,必须要求传入的参数值要大于0,否则输出“温馨提示:边长必须为正数!”;创建一个方法:getArea(),用于计算三角形面积,然后再编写测试类TestTriangle来测试(创建对象、设置属性,调用方法)。

4、小结:封装的具体实现步骤

  • 修改属性的可见性来限制对属性的访问。
  • 为每个属性创建一对赋值方法和取值方法,用于对这些属性的访问。
  • 在赋值和取值方法中,加入对属性的存取限制。

对于我们在Python里创建的Person类,怎样才能将name与character属性设置为私有属性,只能用setXXX()来设置,而不能直接通过对象名.属性名的方式来访问呢?
在这里插入图片描述
此时运行程序,结果如下:
在这里插入图片描述
但是可以通过setXXX()方法来设置对象的私有属性,如下图所示:
在这里插入图片描述
当然还有一种方式也可以访问对象的私有属性,在私有属性之前加一个_类名,如下图所示:
在这里插入图片描述
要保证人品值在[1, 10]之间,我们修改setCharacter()方法:

"""
类的封装
"""

class Person:
    def setName(self, name):
        self.__name = name
        
    def setCharacter(self, character):
        if character >= 1 and character <= 10:
            self.__character = character
        else:
            print("温馨提示:人品值必须在[1, 10]范围内!")
            self.__character = 0
            
    def speak(self):
        print("嗨, 我叫" + self.__name + ",人品值:" + str(self.__character))
        
    def __str__(self):
        return "Person {name='" + self.__name + "', character=" + str(self.__character) + "}"    
        
if __name__ == "__main__":
    p = Person()    
    p.setName("李晓红")
    p.setCharacter(-1);
    print(p)
    p.speak()

运行程序,结果如下:
在这里插入图片描述

二、方法的重载

1、为什么要引入重载

(1)假设场景

编写一个对数字求和的方法,参与求和的数字个数不确定,类型也不确定。

(2)困境所在

由于参与求和数字的个数和类型都不确定,因此要针对不同的情况去设计不同的方法,这样的话非常麻烦,参数个数和类型搭配着变化,导致方法个数实在太多,并且也不便于命名和区分。

(3)解决办法

Java允许在一个程序中定义多个名称相同,但是参数的类型或个数不同的方法,这就是方法的重载。

2、案例演示方法重载

任务1、编程解决数字求和问题

(1)不采用重载进行处理

在这里插入图片描述

package net.hw.lesson10;

/**
 * 功能:实现数字求和
 *      不采用重载
 * 作者:华卫
 * 日期:2020年4月28日
 */
public class Example1001 {
    /**
     * 两个整数求和
     *
     * @param x
     * @param y
     * @return
     */
    public static int add01(int x, int y) {
        return x + y;
    }

    /**
     * 三个整数求和
     *
     * @param x
     * @param y
     * @param z
     * @return
     */
    public static int add02(int x, int y, int z) {
        return x + y + z;
    }

    /**
     * 三个实数求和
     *
     * @param x
     * @param y
     * @param z
     * @return
     */
    public static double add03(double x, double y, double z) {
        return x + y + z;
    }

    public static void main(String[] args) {
        // 调用方法
        int sum1 = add01(100, 150);
        int sum2 = add02(20, 30, 50);
        double sum3 = add03(45.5, 56.2, 89.4);
        // 输出结果
        System.out.println("sum1 = " + sum1);
        System.out.println("sum2 = " + sum2);
        System.out.println("sum3 = " + sum3);
    }
}

针对参数个数或类型不同的情况,分别定义了三个不同的求和方法add01、add02与add03,在调用它们时,千万不能将参数个数和类型给搞错了,用起来显得有点麻烦。

运行程序,查看结果:
在这里插入图片描述

(2)采用重载进行处理

在这里插入图片描述

package net.hw.lesson10;

/**
 * 功能:实现数字求和
 *      采用重载
 * 作者:华卫
 * 日期:2020年4月28日
 */
public class Example1002 {
    /**
     * 两个整数求和
     *
     * @param x
     * @param y
     * @return
     */
    public static int add(int x, int y) {
        return x + y;
    }

    /**
     * 三个整数求和
     *
     * @param x
     * @param y
     * @param z
     * @return
     */
    public static int add(int x, int y, int z) {
        return x + y + z;
    }

    /**
     * 三个实数求和
     *
     * @param x
     * @param y
     * @param z
     * @return
     */
    public static double add(double x, double y, double z) {
        return x + y + z;
    }

    public static void main(String[] args) {
        // 调用方法
        int sum1 = add(100, 150);
        int sum2 = add(20, 30, 50);
        double sum3 = add(45.5, 56.2, 89.4);
        // 输出结果
        System.out.println("sum1 = " + sum1);
        System.out.println("sum2 = " + sum2);
        System.out.println("sum3 = " + sum3);
    }
}

定义了三个同名的add()方法,但是参数个数或参数类型不同,从而实现方法的重载。调用add()方法时,系统会通过传入的参数情况决定调用哪个重载的方法,比如,add(100, 150),自然就会调用add(int x, int y)方法。

运行程序,查看结果:
在这里插入图片描述
注意:方法重载与返回值类型无关,只需满足两个条件:(1)方法名相同;(2)参数个数或参数类型不同。
在这里插入图片描述

任务2、编程解决喂养不同宠物的问题

在这里插入图片描述

(1)创建三个类:Cat、Dog、Bird

每个类都要求两个属性:名字和年龄,还包含一个吃的方法 - eat()。
在这里插入图片描述

  • 创建鸟类(Bird)
package net.hw.lesson10;

/**
 * 功能:鸟类
 * 作者:华卫
 * 日期:2020年4月28日
 */
public class Bird {
    private String name;
    private int 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(name + "爱吃虫。");
    }
}
  • 创建猫类(Cat)
package net.hw.lesson10;

/**
 * 功能:猫类
 * 作者:华卫
 * 日期:2020年4月28日
 */
public class Cat {
    private String name;
    private int 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(name + "爱吃鱼。");
    }
}
  • 创建狗类(Dog)
package net.hw.lesson10;

/**
 * 功能:狗类
 * 作者:华卫
 * 日期:2020年4月28日
 */
public class Dog {
    private String name;
    private int 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(name + "爱吃骨头。");
    }
}

(2)创建学生类Student,重载喂养方法

在这里插入图片描述

package net.hw.lesson10;

/**
 * 功能:学生类
 *      演示重载
 * 作者:华卫
 * 日期:2020年4月28日
 */
public class Student {
    private String name;
    private int character;

    public String getName() {
        return name;
    }

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

    public int getCharacter() {
        return character;
    }

    public void setCharacter(int character) {
        if (character >= 1 && character <= 10) {
            this.character = character;
        } else {
            System.err.println("人品值必须介于[1, 10]之间!");
        }
    }

    public void speak() {
        if (character >= 1 && character <= 10) {
            System.out.println("嗨, 我叫" + name + ",人品值:" + character);
        }
    }

    /**
     * @param cat
     */
    public void feed(Cat cat) {
        System.out.println(name + "喂养了一只猫,名叫" + cat.getName() + ",现在" + cat.getAge() + "岁了。");
        cat.eat();
    }

    /**
     * @param dog
     */
    public void feed(Dog dog) {
        System.out.println(name + "喂养了一条狗,名叫" + dog.getName() + ",现在" + dog.getAge() + "岁了。");
        dog.eat();
    }

    /**
     * @param bird
     */
    public void feed(Bird bird) {
        System.out.println(name + "喂养了一只鸟,名叫" + bird.getName() + ",现在" + bird.getAge() + "岁了。");
        bird.eat();
    }
}

(3)创建测试类TestStudent,调用重载的喂养方法

在这里插入图片描述

package net.hw.lesson10;

/**
 * 功能:测试Student类
 *      主要测试方法的重载
 * 作者:华卫
 * 日期:2020年4月28日
 */
public class TestStudent {
    public static void main(String[] args) {
        // 实例化Cat类
        Cat cat = new Cat();
        cat.setName("欢欢");
        cat.setAge(2);
        // 实例化Dog类
        Dog dog = new Dog();
        dog.setName("瑞瑞");
        dog.setAge(3);
        // 实例化Bird类
        Bird bird = new Bird();
        bird.setName("灵灵");
        bird.setAge(1);
        // 实例化Student类
        Student student = new Student();
        student.setName("杨过");
        student.setCharacter(10);
        // 自我介绍
        student.speak();
        // 调用重载的feed方法
        student.feed(cat);
        student.feed(dog);
        student.feed(bird);
    }
}

运行程序,查看结果:
在这里插入图片描述

三、方法的递归

1、利用递推法与递归法计算阶乘

  • 递推法是由已知向未知拓展,采用循环结构完成,时间复杂度高而空间复杂度低。
  • 递归法是由未知向已知溯源,采用选择结构完成,时间复杂度低而空间复杂度高。

说明:方法的递归是指在一个方法的内部调用自身的过程。递归必须要有结束条件,不然就会陷入无限递归的状态,永远无法结束调用。
在这里插入图片描述

package net.hw.lesson10;

import java.util.Scanner;

/**
 * 功能:计算阶乘
 * 作者:华卫
 * 日期:2020年4月28日
 */
public class Factorial {
    public static void main(String[] args) {
        int n, jc;
        Scanner sc = new Scanner(System.in);

        System.out.print("n = ");
        n = sc.nextInt();

        System.out.println("递推法计算:" + n + "! = " + fact01(n));
        System.out.println("递归法计算:" + n + "! = " + fact02(n));
    }

    /**
     * 递推计算阶乘方法
     *
     * @param n
     * @return
     */
    public static int fact01(int n) {
        int jc = 1;
        for (int i = 1; i <= n; i++) {
            jc = jc * i;
        }
        return jc;
    }

    /**
     * 递归计算阶乘方法
     *
     * @param n
     * @return
     */
    public static int fact02(int n) {
        if (n == 0) {
            return 1;
        } else {
            return fact02(n - 1) * n;
        }
    }
}

运行程序,查看结果:
在这里插入图片描述
方法的调用过程很复杂,下面我们说明整个调用过程。
当n = 10时,对于fact02(10),fact02()将被调用10次,n的值都会递减。当n的值为1时,所有递归调用的方法都会以相反的顺序相继结束,所以的返回值进行累乘,最终得到结果3628800。

1)调用fact02(10),返回fact02(9) * 102)调用fact02(9),返回fact02(8) * 93)调用fact02(8),返回fact02(7) * 84)调用fact02(7),返回fact02(6) * 75)调用fact02(6),返回fact02(5) * 66)调用fact02(5),返回fact02(4) * 57)调用fact02(4),返回fact02(3) * 48)调用fact02(3),返回fact02(2) * 39)调用fact02(2),返回fact02(1) * 210)调用fact02(1),返回fact02(0) * 111)调用fact01(0),根据递归方法的定义,返回1

于是从下而上,依次可以得到fact02(1)、fact02(2)、……、fact02(10)。

大家可以参看完成同样任务的Python代码:
在这里插入图片描述

课堂练习:利用递推法和递归法计算1 + 2 + …… +100

在这里插入图片描述

2、递归案例:汉诺塔游戏

在这里插入图片描述
游戏规则:
(1)一次只能移动一个环
(2)小环必须在大环之上
(3)所有环从A柱移到C柱

在这里插入图片描述

package net.hw.lesson07;

/**
 * 功能:汉诺达游戏
 * 作者:华卫
 * 日期:2020年4月28日
 */
public class HanoiGame {

    private static long step;

    public static void main(String[] args) {
        hanoi(3, 'A', 'B', 'C');
    }

    /**
     * @param n 铁饼个数
     * @param x 起点位置
     * @param y 中间位置
     * @param z 终点位置
     */
    public static void hanoi(int n, char x, char y, char z) {
        if (n == 1) {
            move(x, z);
        } else {
            hanoi(n - 1, x, z, y);
            move(x, z);
            hanoi(n - 1, y, x, z);
        }
    }

    public static void move(char p1, char p2) {
        step++;
        System.out.println("Step " + step + ": " + p1 + " -> " + p2);
    }
}

运行程序,查看结果:
在这里插入图片描述
当铁饼数 n = 3 n=3 ,移动次数 = 2 3 1 = 7 =2^3-1=7

可以测试一下,当铁饼数 n = 10 n=10 时,移动次数有多大。
在这里插入图片描述
可以看到,移动次数 1023 = 2 10 1 1023=2^{10}-1。

四、课后作业

1、创建动物类Animal

创建一个Animal类,包含三个私有属性:名字(name)、类别(category)和年龄(age);编写对三个属性的存取方法,注意年龄必须大于0;创建两个方法:自我介绍(speak)和行走(walk),然后再编写测试类TestAnimal来测试(创建对象、设置属性、调用方法)。
在这里插入图片描述

2、创建复数类Complex

创建复数类Complex,包含实部与虚部两个私有属性,定义复数的加减乘除四则运算方法add()、subtract()、multiply()、divide()。

package net.hw.lesson10;

public class Complex {
    /**
     * 实部
     */
    private double a;
    /**
     * 虚部
     */
    private double b;

    public double getA() {
        return a;
    }

    public void setA(double a) {
        this.a = a;
    }

    public double getB() {
        return b;
    }

    public void setB(double b) {
        this.b = b;
    }

    public Complex add(Complex c) {
        return null;
    }
    
    public Complex subtract(Complex c) {
        return null;
    }
    
    public Complex multiply(Complex c) {
        return null;
    }
    
    public Complex divide(Complex c) {
        return null;
    }
}

大家知道Python语言里有complex数据类型,演示如下:
在这里插入图片描述

3、采用递归方法获取斐波拉契数列第n项的值

斐波那契数列,又称黄金分割数列,指的是这样一个数列: 0 1 1 2 3 5 8 13 21 34 0、1、1、2、3、5、8、13、21、34、…… 在数学上,斐波纳契数列以递推的方法定义: F ( 0 ) = 0 F ( 1 ) = 1 F ( n = F ( n 1 ) + F ( n 2 ) n 2 n N F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)(n≥2,n∈N*) 在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从1963起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

4、采用面向对象方式计算三角形面积

创建三角形类Triangle,包含a、b、c三个私有属性,一个公共方法getArea()。然后创建一个测试类TestTriangle,在其主方法里创建Triangle对象,设置对象属性,调用对象方法得到三角形面积。在这里插入图片描述

五、杂感:“一题多解”与“一诗多译”

俗话讲,殊途同归,条条道路通罗马。

数学课上,常常要求学生,开动脑筋,尽量一题多解;不同的解法往往能反映出解题者不同的着眼点与思维风格,这样做,可以让我在学生的不同解法中体验到多样性的乐趣,而且还可以评判选择比较好的解题方法。程序设计课上,也常常要求学生,针对同一个问题,尽量设计不同算法编写程序来实现,有时看到学生设计出巧妙的算法,心里会有说不出的喜悦。

这学期又讲Java程序设计,觉得蛮好玩的。其中,“多态”是面向对象编程三大支柱之一,分为横向多态与纵向多态。

横向多态,即同一个类中方法的重载(overload),方法名称相同,但是参数的个数或类型不同。那感觉有点类似于同一个厨师,给他不同的食材,可以做出千姿百态的菜品。

纵向多态,即子类对父类方法进行改写或覆盖(override),方法名称与参数完全相同,但是方法的代码不同。那感觉有点类似于长久后浪推前浪,青出于蓝而胜于蓝。

由“一题多解”,自然想到“一诗多译”。横向多态对应横向多译,即不同人对同一首诗进行翻译,给出不同译诗,可以比较不同译者的水平与风格,拓展译者的视野;纵向多态对应纵向多译,即同一个人在不同时期对同一首诗进行翻译,给出不同版本,可以比较该译者水平与风格的变化,挖掘译者的深度。我喜欢一首诗的横向多译,通过比较不同译诗,明白自己的不足,学习别人的长处;同时,我也喜欢一首诗的纵向多译,也许过一段时间,我会重新翻译自己译过的某首诗,希望能译出一点不同的风味来,也算是一种小小的自我突破,感觉蛮有乐趣的。

猜你喜欢

转载自blog.csdn.net/howard2005/article/details/105806388