文章目录:
  1. Lab 2: 类与对象&封装
    1. 实验目的
    2. 注意事项
    3. 1. 类的定义
      1. Question 1
    4. 2. 函数重载
      1. Question 2
    5. 3. 初始化(Initialization)
      1. Quesion 3
      2. Question 4
      3. Question 5
      4. Question 6
    6. 4. 单例模式(Singleton Pattern)
      1. Question 7
      2. Question 8
      3. Question 9
    7. 5. 修饰符
      1. Question 10
    8. 6. 编程题
      1. Question 11
      2. Question 12
  2. 解答:
    1. 1. 类的定义
      1. Question 1
    2. 2. 函数重载
      1. Question 2
    3. 3. 初始化(Initialization)
      1. Question 3
      2. Question 4
      3. Question 5
      4. Question 6
    4. 4. 单例模式(Singleton Pattern)
      1. Question 7
      2. Question 8
      3. Question 9
    5. 5. 修饰符
      1. Question 10
    6. 6. 编程题
      1. Question 11
      2. Question 12
    7. 代码
      1. Q11
      2. Q12


点击展开题目

Lab 2: 类与对象&封装

实验目的

  • 理解并掌握类的概念
  • 理解并掌握对象的概念
  • 理解类与对象的关系
  • 理解面向对象中抽象过程
  • 理解面向对象中的消息
  • 理解 Java 程序的基本结构并能灵活使用
  • 理解并掌握 Java 类的定义(成员变量、成员方法和方法重载)
  • 理解并掌握 Java 类的构造函数(默认构造函数、带参数构造函数),理解重
    载的构造函数并灵活使用
  • 理解 Java 垃圾内存自动回收机制
  • 理解并掌握 Java 类变量和类方法
  • 理解封装含义
  • 理解信息隐藏的必要性
  • 掌握访问控制修饰符的使用

    • 私有成员(变量和方法)的理解和使用
    • 共有成员的理解和使用
    • 保护成员的理解和使用
    • 使用不加任何权限修饰符的成员
  • 加深对“类和对象”的理解

注意事项

本次代码量相对较多,请大家注意文件结构的规划。🫡

随着代码量的增加,建议建立一个自己的统一且良好的代码风格,比如命名风格(camelCase、PascalCase 等)、缩进方式(空格数量、switch-case 缩不缩进等)、开闭大括号换不换行等容易引发战争(迫真)的东西,以养成良好的编程习惯。

比如命名规范,可以参考 Oracle 官网的 Naming Conventions

编程题最好为每一个类编写一个完备的测试类,覆盖尽可能多的输入、函数调用、输出,以证明代码正确实现了功能。

如果编程题使用了 package 语句,应当确保提交时目录结构和 package 语句表达的包结构相同。(IDE 很多时候会帮你做。)

编程题在给出了具体需求的情况下,可以根据自己的需要添加额外的方法,合理即可。

实验报告模板.docx

实验的交付物包含:问答报告(可附带代码文件),实验报告。若附带源码,请使用题号命名每个题目的源码(例如第一题则命名为"Q1")。


1. 类的定义

Question 1

编译 Sample 类,并查看编译结果。

public class Sample {
    int x; // 1
    long y = x; // 2
    
    public void f(int n) {
        int m; // 3
        int t = n + m; // 4
        m = 20;
    }

    public static void main(String[] args) {
        Sample t = new Sample();
        t.f(5);
        System.out.println(t.x);
    }
}
  1. 注释标记的哪些行会导致编译错误?将错误截图,并说明原因。
  2. 区分变量xm,回答谁需要初始化才能使用,为什么

2. 函数重载

Question 2

阅读下面代码

public class Overload {
    Overload(int m) {}

    Overload(double m) {}

    int Overload(int m) {
        return 23;
    }

    void Overload(double m) {}
}

对于 Overload 类,下面哪些叙述是错误的?给出你的回答,并说明原因。

  • a. Overload(int m)Overload(double m) 互为重载的构造方法。
  • b. int Overload(int m)void Overload(double m) 互为重载的非构造方法。
  • c. Overload 类有 2 个构造方法,尝试调用默认构造方法 Overload() 会无法通过编译。
  • d. Overload 类有 3 个构造方法。

3. 初始化(Initialization)

阅读并运行下面这段代码,尝试理解 Java 中初始化的顺序。

class A {
    int value;
    static A a1 = new A(1);

    public A(int i) {
        System.out.println("initialize A" + i);
        value = i;
    }

    public A(A a) {
        System.out.println("copy from A" + a.value);
        value = a.value;
    }

    static A a2 = new A(2);

}

class B {
    A a8;
    // A a7 = new A(a6);
    A a6 = new A(6);
    static A a3 = new A(3);
    static A a4;

    static {
        a4 = new A(4);
    }

    static A a5 = new A(5);

    public B(int i) {
        System.out.println("initialize B" + i);
        a8 = new A(8);
    }

    A a7 = new A(a6);
}

public class Initialization {
    static B b1 = new B(1);
    static B b2;

    public static void main(String[] args) {
        System.out.println("main begins");
        A a9 = new A(9);
        b2 = new B(2);
        System.out.println("main ends");
    }
}

Quesion 3

阅读上面这段代码,给出程序的输出

Question 4

对于非静态属性,它的初始化方法有两种:

  • 在属性定义处显式初始化(如本例中的 a6
  • 在构造方法或非静态方法中初始化(如本例中的 a8

回答下面两个问题(只考虑非静态属性):

  1. 这段代码能够证明“在属性定义处初始化的属性,比在方法中初始化的属性先被初始化”吗?
  2. 这段代码能够证明“在属性定义处初始化的属性,初始化顺序等同于他们在类定义中出现的顺序”吗?

Question 5

请尝试仿照 Question4 的内容,描述静态属性的初始化方式和实际初始化时的顺序

Question 6

已知 static 属性的初始化、static 块的执行,只在 JVM 进行类加载的时候执行,请回答下面的问题

  1. 这段代码能够证明“在类的实例第一次被构造、或类的静态属性和静态方法第一次被访问时,JVM 会执行类加载”吗?如果不能,请尝试修改代码并证明。
  2. 基于static关键字,带有static关键字的方法、变量、代码块可以调用什么?反过来不带有static关键字的方法、变量、代码块可以调用什么?(带有static关键字的方法、变量 or 不带有static关键字的方法、变量 or 都可以)

题外话

懒加载:Lazy Load,对某资源只在需要时才寻找其存在并初始化;对立面是预加载。

预加载:提前加载好所有资源,等待使用资源的那一刻。

对于一些使用频率较低但初始化开销很大的资源,懒加载可以避免他们给程序的初始化增加过多的负担。

JVM 的类加载是懒加载,只有在程序第一次使用到某个类时才去尝试读取其.class文件。类加载只会进行一次,这一次类加载会完成所有的静态初始化工作。更多内容会在后续课程讲解 RTTI 和反射的时候提到。

例如,在游戏编程中,当某一个类的所有实例都使用同一批贴图文件时,可以将贴图资源声明为 static 属性并直接(或在 static 块)初始化。让类的贴图属性引用这些静态资源,这样就可以避免为每一个对象构造单独的贴图文件导致的内存浪费和时间浪费。


4. 单例模式(Singleton Pattern)

阅读下面这段代码,它实现了经典设计模式之一:单例模式。

/**
 * Singleton 一个最简单的单例模式的实现
 */
public class Singleton {
    private static final Singleton uniqueInstance = new Singleton();


    private Singleton() {
    }

    public static Singleton getInstance() {
        return uniqueInstance;
    }

    public void foo() {
        System.out.println("Aha!");
    }
}

Question 7

其他的外部类可以通过 new Singleton() 来构造一个新的 Singleton 变量吗?

Question 8

本题给出的 Singleton 类的写法被称为单例模式,是因为这个类最多只可能有 1 个实例同时存在。为什么只可能有 1 个?这个唯一的实例在什么时候被构造?

Question 9

请写出任意一种外部类调用 Singleton 类的 foo() 的方法。

题外话

这里的 uniqueInstance 初始化方法不是懒加载(Lazy Load)的,因为 uniqueInstance 在类加载时就被初始化了,虽然我们可能最终并用不到它。你可以思考一下如何实现一个懒加载的单例模式。(不要求写在问答报告里。)


5. 修饰符

Question 10

完成下面的表格,打√或×

修饰符同一个类同一个包子类所有类
private
默认(无修饰符)
protected
public

6. 编程题

Question 11

编写程序,在其中定义两个类

  • Person 类:

    • 属性有 nameagesex
    • 提供你认为必要的构造方法;
    • 方法 setAge() 设置人的合法年龄(0~130);
    • 方法 getAge() 返回人的年龄;
    • 方法 work() 输出字符串 working
    • 方法 showAge() 输出 age 值。
  • TestPerson 类:

    • 创建 Person 类的对象,设置该对象的 nameagesex 属性;
    • 调用 setAge()getAge() 方法,体会 Java 的封装性;
    • 创建第二个对象,执行上述操作,体会同一个类的不同对象之间的关系。

除了提交 .java 代码外,你需要在解答中说明如下内容

目录名为:Question11(或者你自己的命名)
文件名有:(如果你放了一个项目进来,则说明你的项目结构以及入口位置)

Question 12

编写一个 Java 命令行程序,只从标准输入读取一行用户输入,判断这行输入是否是一个没有前导 0 的无符号整数;如果是,则还要判断该数字是否是一个回文数。自定义你程序的输出,要求能直观展现程序判断的结果。

对于“没有前导 0 的无符号整数”的定义:

  • 是一个字符串 s
  • s 的长度至少是 1,没有上限要求;
  • s 的字符集 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},其他所有字符都不应该出现在 s 中;
  • s 的长度大于 1 时,若从其首部开始有若干个连续字符 0,那么这些字符 0 都叫做 s 的“前导 0”。

    • 比如数字串 00010020,有三个前导 0
    • 比如数字串 01,有 1 个前导 0
    • 比如数字串 102030,没有前导 0
    • 比如数字串 0,没有前导 0

本题对于回文数的定义:

  • 是一个字符串 s
  • s 的长度至少是 1,没有上限要求
  • s 的字符集 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},其他所有字符都不应该出现在 s
  • s 中的字符逆序排列并去除前导 0 得到的数字串 r ,有 sr 完全相同

    • s = 123 时,r = 321,不相同,s 和 r 都不是回文数
    • s = 12321 时,r = 12321,相同,s 是回文数
    • s = 12100 时,r = 121,不相同,s 不是回文数,但 r 是回文数
    • s = 1 时,r = 1,相同,s 是回文数
    • s = 0 时,r = 0,相同,s 是回文数

本题将输入的一行字符视为一个完整的字符串,如果输入的是诸如“121 121”这样包含空格的串,虽然 121 是回文数,但是整个串不应该被认为是回文数。如果将字符集扩充为包含空格的其他字符集,那么“121 121”就是一个该字符集下的回文串,不过本题的字符集限定为由 0 ~ 9 这十个数字组成的字符集。

除了提交 .java 代码外,你需要在解答中说明如下内容:

目录名为:Question12 (或者你自己的命名)
文件名有:(如果你放了一个项目进来,则说明你的项目结构以及入口位置)

解答:

1. 类的定义

Question 1

  1. 标记的第4行会出现编译错误,错误与第3行的定义也有关。错误截图:

    image-20251016190001370

    原因是标记的第三行定义m时并没有完成初始化,而m为局部变量,必须进行初始化才能使用。而第四行进行t = n + m操作时调用了没有初始化的m,导致编译错误。

  1. m需要初始化才能使用,x不需要初始化。因为m是一个局部变量,Java 中规定局部变量必须在使用前被初始化;而x是类Sample里面的一个成员变量,这种变量在创建时有默认初始值,比如x默认为0,所以不初始化也可以使用。

2. 函数重载

Question 2

3. 初始化(Initialization)

Question 3

initialize A1
initialize A2
initialize A3
initialize A4
initialize A5
initialize A6
copy from A6
initialize B1
initialize A8
main begins
initialize A9
initialize A6
copy from A6
initialize B2
initialize A8
main ends

Question 4

  1. 可以证明“在属性定义处初始化的属性,比在方法中初始化的属性先被初始化”。比如main方法里面执行b2 = new B(2)时,程序先输出的initialize A6copy from A6,再输出的initialize A8,说明属性定义处的a6比方法中初始化的a8更先被初始化。
  2. 可以证明“在属性定义处初始化的属性,初始化顺序等同于他们在类定义中出现的顺序”。在class B里面,a6a7在类中定义出现的更靠前,事实上程序也先输出initialize A6,再输出copy from A6,证明初始化的顺序也是如此。

Question 5

对于静态属性,它的初始化有以下方式:

它的初始化顺序如下:

Question 6

  1. 不能证明具体是被构造、或类的静态属性和静态方法第一次被访问时JVM进行类加载,只可以看出来在执行static B b1 = new B(1);A a6 = new A(6);这些构造时发生了类加载。

    修改代码:

    • 证明第一次被构造时类加载:(仅修改Initialization主类)

      public class Initialization {
          public static void main(String[] args) {
              System.out.println("main begins");
              new A(114514);
              System.out.println("main ends");
          }
      }

      运行结果:

      image-20251016194739300

    • 证明第一次访问类的静态属性进行类加载:

      class C{
          static{System.out.println("initialize C");}
          static int oop = 114514;
      }
      
      public class Initialization {
          public static void main(String[] args) {
              System.out.println("main begins");
              System.out.println("C.oop is " + C.oop); 
              System.out.println("main ends");
          }
      }

      运行结果:

      image-20251016195805109

    • 证明第一次访问类的静态方法进行类加载:

      class C{
          static{System.out.println("initialize C");}
          static void nb(){
              System.out.println("114514 from nb()");
          }
      }
      
      public class Initialization {
          public static void main(String[] args) {
              System.out.println("main begins");
              C.nb();
              System.out.println("main ends");
          }
      }

      运行结果:

      image-20251016195934633

  2. 带有static关键字的方法、变量、代码块可以调用同类中的静态成员(静态字段、静态方法,带static),不可以直接访问实例成员(如实例字段、实例方法,不带static),因为它们属于对象,需要先有实例才可以调用。

    不带有static关键字的方法、变量、代码这些都可以调用。

4. 单例模式(Singleton Pattern)

Question 7

不可以。因为其构造函数是private权限的,任何其他外部类都无法访问。

Question 8

首先由Q7,任何外部类都无法通过构造函数构造一个新的Singleton变量,所以该实例只能在Singleton自己的类里面被实例化。

Singleton类里面只有private static final Singleton uniqueInstance = new Singleton();初始化了一个实例,只有这里有一个实例可以用。

这个唯一的实例在第一次访问或使用Singleton的静态方法(getInstance())时发生类的初始化,此时实例被构造。

Question 9

public class qst9 {
    public static void main(String[] args) {
        Singleton inst = Singleton.getInstance();
        inst.foo();
    }
}

5. 修饰符

Question 10

修饰符同一个类同一个包子类所有类
private×××
默认(无修饰符)同包,不同包××
protected同包,不同包只有子类继承的√,其余××
public

6. 编程题

Question 11

输出结果:

image-20251016203907725

Question 12

代码

Q11

Person.java:

package Question11;

public class Person {
    private String name;
    private int age;
    private String sex;

    public Person() {
    }

    public Person(String name, int age, String sex) {
        this.name = name;
        this.sex = sex;
        if(age>130 || age<0){
            System.out.println("您的年龄不合法!年龄必须为0-130的整数");
            System.out.println("您已成功创建对象,但是目前没有年龄数值,需要手动设定年龄!");
            return;
        }
        this.age = age;
        System.out.println("创建对象成功!");
    }

    public void work(){
        System.out.println("working");
    }

    public void showAge(){
        System.out.println(age);
    }


    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getSex() {
        return sex;
    }

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

    public void setAge(int age) {
        if(age>130 || age<0){
            System.out.println("您的年龄不合法!年龄必须为0-130的整数");
            return;
        }
        this.age = age;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }
}

TestPerson.java:

package Question11;

public class TestPerson {
    public static void main(String[] args) {
        Person li = new Person("李四", 10, "男");
        li.setAge(114514);
        li.setAge(100);
        System.out.println(li.getName() + "的年龄为" + li.getAge());
        Person wang = new Person("王五", 99, "沃尔玛购物袋");
        wang.setAge(114514);
        wang.setAge(100);
        System.out.println(wang.getName() + "的年龄为" + wang.getAge());
    }
}

Q12

Main.java:

package Question12;

import java.util.Scanner;

public class Main {
    static String str;

    // 判断是否为无符号整数
    public static boolean is_shu(){
        int len = str.length();
        if(len < 1){
            System.out.println("输入长度不符合要求!");
            return false;
        }
        for(int i = 0; i < len; i++){
            if(str.charAt(i) > '9' || str.charAt(i) < '0'){
                System.out.println("您输入的不是一个无符号整数呢~");
                return false;
            }
        }
        return true;
    }

    // 判断是否没有前导0
    public static boolean not_has_pre_zero(){
        int len = str.length();
        if(len == 1) return true;
        if(str.charAt(0) == '0') return false;
        return true;
    }

    // 判断是否为回文数
    public static boolean is_huiwen(){
        int len = str.length();
        if(len == 1) return true;
        int mid = (len - 1) / 2;
        for(int i = 0; i <= mid; i++){
            if(str.charAt(i) != str.charAt(len - i - 1)) return false;
        }
        return true;
    }

    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);
        if(s.hasNextLine()) str = s.nextLine();
        if(!is_shu()){
            System.out.println("您的输入不合法,程序已结束");
            return;
        }

        if(!not_has_pre_zero()){
            System.out.println("您的输入为一个前导 0 的无符号整数");
            System.out.println("程序已结束");
            return;
        }else System.out.println("您的输入为一个没有前导 0 的无符号整数");

        if(is_huiwen()) System.out.println("您的输入为回文数");
        else System.out.println("您的输入不是回文数");
        System.out.println("程序已结束");
    }
}