JavaSE 面向对象

文章目录

  • 第十章 面向对象基础-对象
    • 10.1 类与对象
      • 10.1.1 对象是什么
      • 10.1.2 面向过程与面向对象
    • 10.2 实例变量
      • 10.2.1 实例变量的定义
      • 10.2.2 访问实例变量
      • 10.2.3 变量的作用域
      • 10.2.4 堆、栈、方法区
    • 10.3 实例方法
      • 10.3.1 实例方法的定义
      • 10.3.2 调用实例方法
      • 10.3.3 返回值类型
      • 10.3.4 形式参数列表
      • 10.3.5 方法重载
      • 10.3.6 可变参数
    • 10.4 构造方法
      • 10.4.1 构造方法定义
      • 10.4.2 默认无参构造器
      • 10.4.3 this
    • 10.x 总结回顾
    • 10.y 头脑风暴
  • 参考答案
    • 第十章答案

第十章 面向对象基础-对象

内容导视:

  • 类与对象
  • 实例变量
  • 实例方法
  • 构造方法

10.1 类与对象

内容导视:

  • 对象是什么
  • 面向过程与面向对象

问题:

借助韩老师的例子;张老汉也养了两只狗:一只地狱犬,名字叫黑魔王,今年 2340 高寿,红黑相加的花纹十分地炫。

另一只种类为恐惧魔王,名字叫提克迪奥斯,今年 12242 岁,是个不按常理出牌的绿色的恶魔。

要求:当输入名字时,就显示对应的狗的全部信息。

// 狗 1 的所有信息
String dog1Type = "地狱犬";
String dog1Name = "黑魔王";
int dog1Age = 2340;
String dog1Color = "红黑相加的花纹";
String dog1State = "死亡";

// 狗 2 的所有信息
String dog2Type = "恐惧魔王";
String dog2Name = "提克迪奥斯";
int dog2Age = 12242;
String dog2Color = "绿色";
String dog2State = "萎靡不振";

Scanner scanner = new Scanner(System.in);
System.out.println("A:我的狗被你拐到这了?");
System.out.print("B:你好,这里是正规的收容所,");
System.out.print("请输入遗失的狗的名字:");
String name = scanner.next();
if (dog1Name.equals(name)) {
    System.out.print("种类:" + dog1Type);
    System.out.print(",名字:" + dog1Name);
    System.out.print(",年龄:" + dog1Age);
    System.out.print(",状态:" + dog1State);
    System.out.println(";很抱歉,我们已经尽力了!");
} else if (dog2Name.equals(name)) {
    System.out.print("种类:" + dog2Type);
    System.out.print(",名字:" + dog2Name);
    System.out.print(",年龄:" + dog2Age);
    System.out.print(",状态:" + dog2State);
    System.out.println(";这就还给你!");
} else {
    System.out.println("对不起,我们没有查到此狗的信息!请你到别处咨询!");
}

使用局部变量将所有信息拆卸,数据管理麻烦,将来也不好利用,只能将一个个的变量作为方法实参。

目前就两只狗,如果有 n 条狗,那就需要定义 5n 个变量,效率低下。

如果使用数组:

String[] dog1 = {"地狱犬", "黑魔王", "2340", "红黑相加的花纹", "死亡"};
String[] dog2 = {"恐惧魔王", "提克迪奥斯", "12242", "绿色", "萎靡不振"};

数据类型无法得到体现,因为数组只能存储同一种类型的数据。

只能通过下标来取数据,造成变量名与内容对应关系不明确,比如单看代码,究竟地狱犬是名字还是黑魔王是名字,没有提示,还有 “2340” 是什么;而使用 String dog1Name = "黑魔王"; 一看就知道这是第 1 只狗的名字。

10.1.1 对象是什么

对象

每个可以进行研究的任何事物都可以当作一个对象,如整数、棋盘、沙发、狗、飞机…,每个对象都有自己的行为和状态,比如他家的狗的状态有颜色、品种、名字…;行为、动作有吃、睡觉、玩耍…。

状态也可以被称为属性,实际指代每个对象的数据(信息),但属性还另有他指,注意区分别混淆。

通过观察现实的种种事物,把具有共同特征的一类事物抽象出来得到一个类;比如金毛犬、比特犬、田园犬都是狗,是狗类,再抽象一点就是动物类,可以称狗类是动物类的子类。

用类 class 描述这类事物的共同特征(行为和状态);用字段 field 代表状态,方法 method 代表行为。

当然也可以把类当作组织变量、让变量之间形成联系的一种方式。

// 自定义的狗类
class Dog {
    // 字段,保存了狗的状态
    String type;
    String name;
    int age;
    String color;
    String state;
    
    // 方法,表示狗的行为
    void eat(){
        System.out.println(name + "边吃饭边打 Tom 猫。");
    }
    void sleep(){
        System.out.println(name + "满足的打起了瞌睡。");
    }
    
    // 构造器与继承来的方法以后再讲
    public Dog(String type1, String name1,
               int age1, String color1, String state1) {
        type = type1;
        name = Objects.requireNonNull(name1);;
        age = age1;
        color = color1;
        state = state1;
    }
    @Override
    public String toString() {
        return "Dog{种类:" + type + ",姓名:" 
            + name + ",年龄:" + age + ",颜色:" + color 
            + ",状态:" + state + "}";
    }
}

实例化

既然类描述了某类事物的共同特征,那么可以把类当成创建对象的模板,只需要创建对象时指定不同的参数,如年龄、品种…,就可以得到不同的具体对象(实例),这个过程就叫实例化。

例:金毛、比特都是狗,甚至同一种品种的狗,但不同的狗性别、年龄、习惯各有不同。可以把 Dog 类当作模板,使用 new 关键字(调用构造方法传进参数)创建很多的狗,创建的每个狗都是一个实例 instance。

还记得之前讲的引用数据类型吗,我们自定义的类型 Dog 就是。

JavaSE 面向对象_第1张图片

那么之前的问题就可以解决了:

创建人类:

class Person {
    // 主人的姓名
    String name;
    // 主人拥有的狗
    List<Dog> dogList = new ArrayList<Dog>(3);
    
    // 收养狗的方法
    public void add(Dog... dogs) {
        for (int i = 0; i < dogs.length; i++) {
            dogList.add(dogs[i]);
        }
    }
    
    // 寻找狗的方法
    public void find(String dogName) {
        System.out.println("你每天都在找狗...");
        for (Dog dog : dogList) {
            if (dog.name.equals(dogName)) {
                System.out.print("你脑海浮现出一段信息:");
                System.out.println(dog);
                System.out.println("你突然想起,这只是回忆罢了,都已过去很久了。");
                return;
            }
        }
        System.out.println("你甩了甩脑袋,记忆已经模糊不清了。");
    }
    
    public Person(String name) {
        this.name = name;
    }
}

在入口方法中勾勒出不同对象之间的行为、交互:

public static void main(String[] args){
    // 以狗类为模板创建两个狗对象
    Dog dog1 = new Dog("地狱犬", "黑魔王", 2340, "红黑相加的花纹", "死亡");
    // 使用 new 关键字创建某引用类型的对象,同时调用对应gou'z
    Dog dog2 = new Dog("恐惧魔王", "提克迪奥斯", 12242, "绿色", "死亡");
    
    // 以人类为模板创建一个主人对象
    Person zhangSan = new Person("张三");
    
    // 人类收养了两只狗
    zhangSan.add(dog1, dog2);
    
    // dog1 睡觉时遗失了
    dog1.sleep();
    System.out.println("突然有一天再也找不到" + dog1.name + "了。");
    
	// 人类开始找狗
    zhangSan.find(dog1.name);
}

虽然设计类比较麻烦,但是比之前,数据的联系更紧密,使用 dog1 这个引用就可以获取此狗的所有信息,如 dog1.name,dog1.age,一目了然。

像 dog1、dog2 变量,它保存了对象的内存地址,这样的变量称为对象引用。可以把引用当作电视遥控器,而对象是电视机,通过遥控器访问电视机的不同频道和调节音量大小。

补充来说,变量有数据类型、变量名、存储的值。dog1 是变量名,使用变量名访问保存的数据。如果变量保存的值是对象的内存地址,则这个变量称为引用,真正的对象在堆中。

关于“引用保存了对象的内存地址,指向了堆中的对象,可以通过引用修改对象的状态”我一直抱有疑问,引用真的保存了对象的内存地址吗?究竟是如何通过引用影响对象的,无奈隐藏的比较深,暂时看不出来。

JavaSE 面向对象_第2张图片

首先在方法区加载相关类的信息、静态变量初始化,执行到 Dog dog1 这行代码时使用 new 创建了一个狗对象,在堆中开辟空间存储对象的实例变量,赋默认值,再调用构造器完成实例变量的初始化,最后把地址赋给 dog1 引用。

字符串是引用类型,字符串一般放在方法区中的字符串常量池,如对象的 type 字段保存的是字符串的内存地址。

JDK7 时字符串常量池被移到了堆中。

10.1.2 面向过程与面向对象

其实我有我自己的理解,但是一搜,有那么多博客都转载类似的言论,那就姑且当作是正确的吧。

面向过程

碰到了一个问题,需要解决。面向过程是分析出解决问题所需要的步骤,先做什么,再做什么…然后用方法把这个步骤一步一步实现,使用时依次调用就可以了。(自顶而下、逐步求精)

例:新学期学生到校注册流程

JavaSE 面向对象_第3张图片

假如这些信息要存到 txt 文件中,按照以上步骤解决问题,如第一步把学生的年龄、身份证号等写入文件中,编写一个方法实现它,需要的时候再调用。(若某一步太过繁杂,难度很大,可以将此步再次细分)

public static void main(String[]args) {
    // 准备相关变量保存数据
    String schoolName = "怕踢中学";
    String studentName = "阿衰";
    .....
        
    // 把年龄和身份证号写入登记表文件中...
    	// 获取输出流
        // 把年龄,身份证号写入
    // 减去学生银行卡号的钱,增加学校的钱...
        // 先判断余额是否大于支付金额
        // 是否支付成功
        // 增加学校钱
        // 哪一步失败就回退
  	// 把经过认证过的人的信息添加到班级中,成为学生...
        // ...
}

面向对象

不再针对解决问题的先后步骤,而是先从问题中分析出完成事件的有哪些要素和参与对象,设计好模板(类),创建出对象,描述出各个对象在整个过程中发生的动作与行为、与其他对象的交互。

例:上述有学生、老师、学校、班级,可以先定义类描述共同特征,通过实例化创建对象,让这些对象去执行相应的动作完成学生的注册。

入口类:

public static void main(String[]args) {
    // 创建对象
	Student aShuai = new Student(12, "420425201209099090", "阿衰");
    School school = new School("怕踢中学");
    
    school.registerStudent(aShuai, 9000);
}

用到的类:

public class Student {
    private int age;
    private String idNumber;
    private String name;
    ...
    
    public Student(int age, String idNumber, String name) {
        setAget(age);
        setIdNumber(idNumber);
        setName(name);
    }
    
    public void decrease(School school,int money) {
        // 扣阿衰的钱...
        	// 获取阿衰的银行卡号
        	// 卡减钱
        	// 给学校加钱
    }
}
public class School {
    private String name;
    ...
    
    // 注册流程
    public boolean registerStudent(Student studnet, int money){
        // 阿衰给学校加钱
        aShuai.decrease(school, money);
        
        // 怕踢中学添加阿衰的信息
    	school.addStudent(aShuai);
    
        // 班级添加阿衰信息
        c.addStudent(aShuai);
    }
    ...

面向过程与面向对象的区别

面向过程

优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix 等一般采用面向过程开发,性能是最重要的因素。

缺点:没有面向对象易维护、易复用、易扩展的优点。

面向对象

优点:易维护、易复用、易扩展;由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。

缺点:性能比面向过程低。

自己的想法

面向对象并不与面向过程对立,面向过程的程序设计是面向对象的基础,而从全局的角度来看采用面向对象,但对象调用的方法内部仍是面向过程思想来设计的,如阿衰扣钱的方法里内部使用面向过程的方法解决。

当逻辑比较简单时,使用面向过程能够更快的实现功能,而面向对象要设计出需要的类,考虑需要字段和方法,很麻烦;

当逻辑比较复杂时,采用面向对象的思维方式更贴近生活,相比与面向过程零零散散毫无联系的代码,更容易维护,在合理使用配置文件和接口后,可以做到基本不修改原有代码,就可以新增功能;而面向过程一环扣一环,每一步过程的结果都是下一步的前提条件,有了新的需求,又要全盘修改。

面向对象在我看来就是分类整理,同类数据归于一类。

10.2 实例变量

内容导视:

  • 实例变量的定义
  • 访问实例变量
  • 变量的作用域
  • 堆、栈、方法区

10.2.1 实例变量的定义

定义在方法外、类体内的变量称为字段(field),也称成员变量、全局变量。

字段是类的组成部分,字段的数据类型可以是基本数据类型,也可以是引用数据类型。(数组、接口、其它类)

一个类拥有的东西(has a),可以以字段的方式存在。

class Person{
    String name;
    int age;
    String id;
    static String nationality;
    String PhoneNumber;
}

成员变量分为实例变量、静态变量。简单区分,带 static 关键字的字段就是静态变量否则就是实例变量。

我们之前的例子绝大部分都是实例变量。通过引用.变量名访问、赋值。

定义实例变量

访问权限修饰符 数据类型 变量名;
private String name;
protected Dog dog;

访问权限修饰符,是用于控制访问范围,有 4 种:public、protected、默认(什么都不写)、private。用在变量上,是控制变量的访问范围,用在方法上是控制方法使用的范围。

你可以先试下,后面封装时再讲。

class Dog {
    private int age;
    int age;
    String name;
    boolean sex;
}
class Hello {
    public static void main(String[]args){
        Dog dog = new Dog();
		int dogAge = dog.age;
    }
}
// 错误: age 在 Dog 中是 private 访问控制
// 有 private 修饰的字段,无法在其他类使用引用.字段名访问

字段如果不赋值,有默认值,与数组元素默认值一致,这里不再赘述。

Dog dog = new Dog();
System.out.println(dog.age);// 0
System.out.println(dog.name);// null
System.out.println(dog.sex);// false

10.2.2 访问实例变量

首先我们需要创建对象,然后通过引用访问实例变量。

创建对象

引用数据类型 变量名 = new 引用数据类型();
Dog dog = new Dog();

也可以先声明,后赋值:

Dog dog;// 此时变量没有保存任何值
dog = new Dog();// 开辟存储空间创建一个对象,把对象的地址赋给 dog,dog 被称为对象引用

引用类型的变量可以赋值为 null,代表空,没有指向任何对象;使用值为 null 的引用访问对象的实例变量、实例方法时,就会报 java.lang.NullPointException 空指针异常。

Dog dog = null;
int age = dog.age;

访问实例变量

引用名.字段名

System.out.println(dog.name);
int name = dog.name;// 使用变量接收

赋值

dog.name = "大黄";

10.2.3 变量的作用域

全局变量的作用域为整个类体,局部变量的作用域为声明它时的域({})中。

class Test {
	int age = 22;// 作用域为整个类体,如 some 方法中可以访问到 age
    
    public void some() {
        System.out.println(age);// 2
        //System.out.println(i);// 找不到符号,变量 i
    }
    
    public void other() {
        int i = 3;// 局部变量 i 只在 other 方法内有效
    }
}

全局变量可以不赋值直接使用,因为有默认值;局部变量必须赋值后才能使用。

其他规则

  1. 全局变量可以与局部变量重名,直接访问变量时,遵循就近原则。

    class Person {
        int age;// 实例变量
    
        public void some() {
            int age = 5;
            String name = "小名";
            System.out.println(age);// 5
            System.out.println(name);// 小名
    
            /*
            	要是实在想访问实例变量 age
            	this 代表当前调用此方法的对象
            */
            System.out.println(this.age);// 0
        }
    }
    
  2. 实例变量和静态变量的生命周期较长,实例变量伴随对象的创建而分配空间,伴随对象的销毁而销毁;静态变量只要程序加载了此类,该类的静态变量就会被分配空间,程序结束后释放空间。

    局部变量生命周期较短,伴随代码块、方法的执行而被创建,执行结束被销毁。比如调用方法时会在栈中开辟空间,创建局部变量,方法执行结束后,栈空间被释放,局部变量被销毁。

  3. 全局变量可以被本类和其它类使用(只要有对象引用即可),局部变量只能在本类声明它时的域中使用。

  4. 不同方法之间的局部变量即使名字相同,也不是同一块空间。(以后还会详细说明)

    public static void some(int i) {
        i = 6;// 你修改的是这个方法的 i 变量保存的值,不影响下面的 i
    }
    public static void main(String[] args) {
        int i = 3;
        some(i);
        System.out.println(i);// 3
    }
    
  5. 局部变量不能加访问权限修饰符 和 static 修饰符。

10.2.4 堆、栈、方法区

粗略的介绍,因为这东西不是能够一眼看穿,比较底层,难以感知它的存在。

方法区

方法区最先有东西,存储类的加载信息(代码片段)、字符串常量池、静态变量等。

方法被调用时,在栈中分配空间,存储局部变量(基本数据类型的值和对象的内存地址)及运行过程需要的内存。

数据进来叫“进栈”、“入栈”、“压栈”、“push”,出去叫“出栈”、“弹栈”、“pop”。最先进来的最后出去,最后进来的最先出去。

JavaSE 面向对象_第4张图片

每调用一个方法,就会开启新的一个栈(方法不调用,不会开启新栈),压在原有栈的上面。栈帧指的是栈顶部的元素(如现在的 b),只有栈帧才有活跃权,调用者 a 需要等待被调用的方法 b 执行完毕释放空间后才能执行接下的代码。

当最上面的栈执行完毕后,就会释放空间,轮到下一个。

每个栈中的局部变量是相互独立的,不会互相影响。

当不停的开启新栈,而不释放空间,栈内存满了会报 java.lang.StackOverflowError 栈溢出错误。方法递归和循环引用时,最容易出现此问题。

class Test{
    Test test = new Test();
    public static void main(String[] args) {
        new Test();
        /*
        	new Test();创建了一个对象,会调用此无参构造完成初始化
        	而无参构造会先调用父类即 Object 的无参构造方法;
        	
        	接着显示初始化实例变量 test,然而 Test test = new Test();
        	也创建了对象,会调用无参构造,而无参构造会先调用父类即
            Object 的无参构造方法;接着显示初始化 test...
        	
        	一直循环下去,直到栈内存和堆内存总有一个空间不够
        */
    }
}

使用 new 关键字创建的对象都会保存在堆中。对象中的实例变量、包括从父类继承过来的实例变量都有默认值,通过调用构造器完成对象的初始化。

堆只有一个,被所有栈共享。

short、byte、int、long 类型的字段的默认值是 0
float、double 是 0.0
char 是空,对应十六进制是 0x0000
boolean 是 false
String 和其它引用类型是 null

直接输出引用时,会自动调用 toString 方法(是从顶级父类 Object 继承得来的),toString 方法默认会返回对象数据类型的完整类名@十六进制的哈希码值

hashCode 方法会返回一个整数(哈希码值),哈希码值是将对象的内部地址转为整数得到的,可以看做是对象的内存地址。同一个对象调用 hashCode 方法时,返回的哈希码值一定相同。

10.3 实例方法

内容导视:

  • 实例方法的定义
  • 调用实例方法
  • 返回值类型
  • 形式参数列表
  • 方法重载
  • 可变参数

通常我会将实例变量与实例方法统称为实例相关的。

10.3.1 实例方法的定义

定义在类体中的方法称为成员方法,对应对象的行为。

成员方法分为实例方法和静态方法。

简单的讲下它们的区别:实例方法需要创建对象,使用引用.方法名调用。而静态方法多了一个 static 修饰,使用类名.方法名调用,如果在同类下可以省略类名。

也就是说光定义方法不用,方法里的语句是不会执行的;换言之,因为 main 方法是由 JVM 调用,那么我们需要在 main 方法中写调用此方法的语句即可。

定义实例方法的语法

访问权限修饰符 返回值数据类型 方法名(形式参数1, 形式参数2, ...) {
    方法体
}

方法体由一句句的 java 语句构成;

方法名遵循驼峰命名,在实际工作中,要见名知意,表明这个方法的功能。

我们先从最简单的没有返回值 void、没有形式参数列表讲起。

10.3.2 调用实例方法

想要调用某类的实例方法,首先需要此类的实例,通过引用.方法名调用。

class Test {
    public void some() {
        System.out.println("执行 some 方法中的语句...");
    }
    public static void main(String[] args) {
        // 先创建此类的实例,再通过引用.方法名调用
        Test test = new Test();
        
        test.some();
        // new Test().some(); 如果此对象不再使用,这样也行
    }
}

另外我还听过一种有趣的说法:“调用方法的行为有时被称为向对象发送消息。面向对象编程可以总结为:向对象发送消息”。

此外在实例方法中调用本类方法,直接通过方法名调用即可。

public void some() {
    other();
}
public void other() {
    System.out.println("其它方法");
}

因为调用实例方法,一定先要有实例,如 引用.some(),那么当执行到 other()这句时,就是此实例在调用 other 方法,所以引用.可以省去。

方法的好处

需求:遍历 10 遍二维数组

int[][] arr = {{52, 621, 1, 5}, {14, 51}, {1, 5, 52}};

for(int i = 0; i < arr.length; i++) {
    for(int j = 0; j < arr[i].length; j++) {
		System.out.print(arr[i][j] + "\t");
	}
    System.out.println();
}
for(int i = 0; i < arr.length; i++) {
    for(int j = 0; j < arr[i].length; j++) {
		System.out.print(arr[i][j] + "\t");
	}
    System.out.println();
}
...

这时需求又改了,在遍历之前打印“好嗨呦”,捉急啊!

但是如果将遍历封装成一个方法:

public void toString(int[][] arr) {
    System.out.println("好嗨呦");
    for(int i = 0; i < arr.length; i++) {
        for(int j = 0; j < arr[i].length; j++) {
            System.out.print(arr[i][j] + " ");
        }
        System.out.println();
    }
}

然后使用引用调用此方法即可;现在代码不长,感觉没什么,以后几百行,千行代码完成一个功能,你在 A、B、C…类都要用这个方法,难道还要手动把这些代码复制到 A、B、C 的 main 方法内吗?如果把代码放入某类的方法中,只需要写一次,让其它类通过方法名调用即可。

方法提高了代码的复用性,可以把它当成为了完成某个特定功能,且可以重复利用的代码片段。

将实现的细节封装起来,其它人只需看 API 文档知晓方法作用后调用即可,不需要关心底层细节,如 Arrays.sort(arr); 你并不需要了解排序算法,也能够使用它对数组进行排序。

10.3.3 返回值类型

返回值类型为 void

void 代表什么都不返回。

其它返回值类型

如果期待方法执行完毕后需要返回一个值,这个值的类型就是返回值类型;使用 return 值;。(如果返回的值可以自动转成返回值类型也算)

一个方法最多只能返回一个值,由于 return 语句的执行代表着方法的结束,程序回到调用者处,所以 return 只能是最后执行的一条语句。(确保 return 语句一定能够执行)(同一个域中,在 return 之后不能再有其它语句,否则执行不到,是无效的语句)

作为调用者而言,可以接收有返回值的方法返回的值,也可以不接收。

// int 代表返回值的类型为 int,或者可以自动转成 int 类型
public int some() {
    ...
    return 4;
}
public void other() {
    // 调用 some 方法,可以选择接收返回值,或者不接收
    some();
    int num = some();
}

10.3.4 形式参数列表

方法括号中定义的变量,简称形参,每个参数都是局部变量,调用方法时传入实参赋值:

  • 参数可以是 0 到 n 个
  • 参数之间使用逗号分隔
  • 调用方法时,按对应类型、个数、顺序传入实际参数。
public void some(int a, String b, char c) {}

比如上面,那我们调用时传入三个实参,分别是 int、String、char 类型

引用.some(5, "你好", 'a');

这相当于

int a = 5;
String b = "你好";
char c = 'a';

也可以使用变量作为实参。

值传递

之前在一维数组的数组赋值机制中,也说过 Java 采用值传递,方法接收的是实参值的拷贝。一个方法不能修改实参保存的值,但可以修改实参指向的实例的状态。

基本数据类型:

public static void main(String[] args) {
    int i1 = 100;
    some(i1);
    System.out.println(i1);// 100
}
public static void some(int i2) {// 与变量名无关,可以把 i2 改成 i1
    i2 = 55;
}

JavaSE 面向对象_第5张图片

引用数据类型:

public static void main(String[] args) {
    char[] c1 = {'h','a','i'};
    some(c1);
    System.out.println(c1);// hai
}
public static void some(char[] c2) {// 与变量名无关,可以把 c2 改成 c1
    c2 = null;
}

JavaSE 面向对象_第6张图片

public static void main(String[] args) {
    char[] c1 = {'h','a','i'};
    some(c1);
    System.out.println(c1);// wai
}
public static void some(char[] c2) {// 与变量名无关,可以把 c2 改成 c1
    c2[0] = 'w';
}

JavaSE 面向对象_第7张图片

10.3.5 方法重载

方法签名

方法名与形式参数列表合并称为方法签名(signature of the method),方法签名作为方法的唯一标识,同类中方法签名不能重复,即同一个类不能存在重复的方法,否则就无法区分调用的是哪个方法。

方法重载

同一个类中,方法名相同,但形参列表不同的两个方法构成重载。

方法名相同很好理解,而形参列表不重复,是指参数的个数、类型、顺序至少有一样不同。

如下面就构成重载:

public void some(int i, String s) {}
public void some(int i, String s, char c) {}
public void some(char c, boolean b) {}
public void some(String s, int i) {}

注意了,顺序不同,是指不同类型的顺序,而不是靠变量名区分顺序:

public void some(int i, int s) {}
public void some(int s, int i) {}// some(int,int)方法已定义

返回值类型、变量名都与方法重载无关,这个很好理解:

public void some(int i) {}
public int some(int j) {return 1;}

现在使用调用方法:引用.some(2); 是不是没办法区分调用的是谁?返回值有但我可以不接收。

方法重载的好处

减轻了起名与记名的麻烦,拿打印方法举例:

JavaSE 面向对象_第8张图片

10.3.6 可变参数

可变参数的本质是数组,可以直接将同类型的数组实例当成实参。

类似于将同名、参数类型相同但参数个数不同的方法封装成了一个方法。

语法

数据类型... 变量名
String... strs    

它代表着 0 ~ n 个此数据类型的参数。

public int sum(int... nums) {
    int sum = 0;
    for (int i = 0; i < nums.length; i++) {
        sum += nums[i];
    }
    return sum;
}

调用此方法,可以传递 0 ~ n 个 int 类型的实参,如:

引用.sum();
引用.sum(1);
引用.sum(1, 5);
引用.sum(4, 6, 2, 7, 5);
...
int[] arr1 = {4, 5, 3};
引用.sum(arr1);
引用.sum(new int[5]);
引用.sum(new int[]{4, 5, 3});

由于可变参数长度无法确定,所以它只能放在形参列表中的最后 1 个位置;换言之,形参列表中最多只能出现一个可变参数。

10.4 构造方法

内容导视:

  • 构造方法定义
  • 默认无参构造器
  • this

10.4.1 构造方法定义

构造方法也称构造器,调用时在栈中分配空间,是一种特殊的方法,定义时不用写返回值类型,方法名必须与类名一致!

语法:

访问权限修饰符 类名(形参列表) {
    方法体...
}

在调用构造方法之前,对象已经创建好了,在堆中分配了空间,实例变量有默认值;构造方法执行结束后,返回这个对象的地址,赋给引用,从而让外部程序可以访问到此对象。(如果实例只使用一次也可以不赋给引用,直接调用实例方法)

一般我们通过调用构造方法来完成实例变量的初始化,在创建对象时根据传入实参的不同调用对应构造器。对的,构造器可以不止一个,构成重载。

class Person {
    String name;
    int age;
    // 构造器1
    public Person(String name1) {
        // 接收到实参,将实参保存的值赋给 name
        name = name1;
    }
    
    // 构造器2
    public Person(int age1) {
        age = age1;
    }
}
class Test {
    public static void main(String[] args) {
        // 调用构造器1,传入实参"张三"
        Person zs = new Person("张三");
        System.out.println(zs.name);// 张三
        
        // 调用构造器2
        Person ls = new Person(5);
        System.out.println(ls.age);// 5
        
        System.out.println(zs.age);// 0
        System.out.println(ls.name);// null
    }
}

虽然调用的可能是同一个构造器,通过传入实参的不同,每个实例都有自己独有的一份数据。

JavaSE 面向对象_第9张图片

10.4.2 默认无参构造器

不知道你们发现没有?如果手动定义了有参构造器,则 new Test()就会报错;

class Test {
    public Test(int name) {}
    public static void main(String[] args) {
        new Test();
    }
}

这是因为当没有定义构造器时,编译器会自动生成一个默认的无参构造器(缺省构造器),它的访问权限修饰符与声明类时的访问权限修饰符一致;当定义了构造器时,默认无参构造就失效了;可以通过反编译 javap 类名 命令测试。

class Test {}
// 编译后生成了 Test.class,再执行 javap Test,结果如下:
class Test {
	Test();
}
public class Test {
	public Test(int i) {}
}
// 反编译字节码,可以看到没有提供无参构造
public class Test {
	public Test(int);
}

面对这种情况,我们通常会手动添加无参构造。

10.4.3 this

对象创建时,JVM 会给对象分配一个指向自身的引用 this,this 保存着自身的内存地址。

class Person {
    String name;
    
    public void setName(String name1) {
        // this 指代正在初始化的对象
        this.name = name1;
    }
}
Person p1 = new Person();
Person p2 = new Person();

p1.setName("张胜男");
p2.setName("张三");

// 可以通过引用.变量名区分
String name = "它";
p1.name = name;
p2.name = "不好";

this 类似于中文的“自己”,“自己”指的不是“张三”,也不是“李四”,而是说出这句话的人。

在实例方法中:

this 指向 “调用此方法的实例”,实例方法肯定需要实例调用,那么谁调用的方法,this 就是谁。

p1 实例调用 setName 方法,那么方法中的 this 指向的就是 p1 实例;p2 实例调用 setName 方法,那么 setName 方法中的 this 指向的就是 p2 实例。

一般情况下,this. 可以省去;当局部变量与实例变量重名时,需要添加 this. 加以区分。

class Person {
    String name;
    int age;
    String hobby;

	// 由于可以通过 this. 区分,所以没必要刻意取名
	public Person(String name, int age, String hobby) {
        this.name = name;
        this.age = age;
        this.hobby = hobby;
    }
}

除了访问字段,this 也可以调用方法,this.方法名(实际参数列表)。

this 在实例方法中作为第一个参数(隐含)

class U {
    public void some(U this) {
        // this. 可以省略
        this.other();
    }
    public void other() {}
}

调用其它构造器

this(实际参数列表),只能在构造方法中的第一句出现。

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

可以减少重复代码,不过这种方式,当实例变量太多时,很容易就不小心就传错参数,看着也很累赘。

new Person("爱好:打篮球", 55, "姓名:坊仓");

可以使用链式编程,想要初始化哪些字段,就调用对应方法完成赋值。

class U {
    int a, b, c, d;

    public U a(int a) {
        this.a = a;
        return this;
    }
    public U b(int b) {
        this.b = b;
        return this;
    }
    public U c(int c) {
        this.c = c;
        return this;
    }
    public U d(int d) {
        this.d = d;
        return this;
    }
}
class Test {
    public static void main(String[] args) {
        U u = new U().a(5).b(6).c(7).d(8);
    }
}

this 只能在实例方法、构造器中使用。(因为访问静态方法不需要实例)

10.x 总结回顾

数据类型 引用名 = new 数据类型();
引用名.实例变量名
引用名.方法名(实际参数列表)    

构造方法用于完成实例变量的初始化;this 指代自身,只能出现在与实例相关的地方。

同一个类中,方法名相同但形式参数列表不同(顺序、个数、类型至少有一个不同)的方法构成重载。

每调用一个方法就是开启了一个新的栈,栈中存储局部变量及运行过程需要的内存;不同方法内创建的局部变量即使名字相同,也不是同一块内存空间,互不影响。

创建的对象存储在堆中。

10.y 头脑风暴

10.1 在 AA 类编写一个静态方法,判断一个整数是奇数还是偶数,奇数返回 false,偶数返回 true。

10.2 分析如下代码,控制台上会输出什么?

1)在类 A 中:

public void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    System.out.println("swap 中 a:" + a);
    System.out.println("swap 中 b:" + b);
}

public static void main(String[] args) {
    int a = 5;
    int b = 1;
    new A().swap(a, b);
    
    System.out.println("main 中 a:" + a);
	System.out.println("main 中 b:" + b);
}

2)在类 B 中:

public void some(int[] arr) {
    arr[0] = 1234;
}
public static void main(String[] args) {
    int[] arr = new int[3];
    new B().some(arr);
    System.out.println(arr[0]);
}

10.3 类 Person 中,分析输出什么?

1)

int age;
public void some(Person p){
    p = null;
}

public static void main(String[] args) {
    Person p = new Person();
    p.age = 32;

    p.some(p);
    System.out.println(p.age);
}

2)

int age;

public Person some(Person p) {
    return p;
}

public static void main(String[] args) {
    Person p = new Person();
    p.age = 32;

    Person p1 = p.some(p);
    System.out.println(p == p1);
    System.out.println(p.age == p1.age);
}

3)

int age;

public Person some(Person p) {
    Person p1 = new Person();
    p1.age = p.age;
    return p1;
}

public static void main(String[] args) {
    Person p = new Person();
    p.age = 32;

    Person p1 = p.some(p);
    System.out.println(p == p1);
    System.out.println(p.age == p1.age);
}

4)

Person p = new Person();

Person p1 = p;
p1.age = 2;
p1 = new Person();
p1.age = 333;

System.out.println(p.age);

10.4 一共有 ?个桃子。每天猴子吃其中的一半加 1 个桃子,当第 10 天时,准备吃,一看,只剩 1 个桃子了,请问 ?的值为?

10.5 有 1 个迷宫由二维数组组成。1 代表墙,0 代表可通行。现有一球在左上角(1,1)处,要求到右下角(7,7),记录小球走过的路径。

1 1 1 1 1 1 1 1 1
1 q 0 0 0 0 0 0 1
1 0 1 0 0 0 0 0 1
1 0 1 0 0 0 0 0 1
1 1 1 0 0 0 0 0 1
1 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 1
1 1 1 1 1 1 1 1 1

10.6 如图,把 A 中的 n 个圆盘全部移到 C 中。要求:大圆盘不能放在小圆盘上面,每次只能移动一个圆盘,如何移动。

JavaSE 面向对象_第10张图片

10.7 在一个 8*8 格的棋盘摆放 8 个棋子,要求:任意两个棋子之间不能处于同一行、同一列、同一斜线。共有几种摆法?

10.8 设计一个类,完成与电脑的猜拳,电脑随机出石头、布、剪刀,要求显示输赢次数。

参考答案

第十章答案

10.1 在 AA 类编写一个静态方法,判断一个整数是奇数还是偶数,奇数返回 false,偶数返回 true。

考虑返回一个 boolean 类型的值,判断一个整数是否是偶数,那么需要一个 int 类型的参数。

class AA {
    public static boolean isEvenNumber(int num) {
        if (num % 2 == 0) {
            return true;
        }
        return false;
    }
}

也可以直接返回 return num % 2 == 0;


10.2 分析如下代码,控制台上会输出什么?

1)在类 A 中:

public void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    System.out.println("swap 中 a:" + a);
    System.out.println("swap 中 b:" + b);
}

public static void main(String[] args) {
    int a = 5;
    int b = 1;
    new A().swap(a, b);
    
    System.out.println("main 中 a:" + a);
	System.out.println("main 中 b:" + b);
}

JavaSE 面向对象_第11张图片

当代码执行到 swap(a,b)这行时,会开启一个新栈,分配此方法运行所需要的内存空间,同时将实参赋给形参。

JavaSE 面向对象_第12张图片

只有栈顶部的元素才有活跃权,调用者 main 陷入等待,直到 swap 方法执行完毕后才能执行接下的代码。

执行 swap 中的代码,int temp = a = 5; a = b = 1; b = temp = 5;

JavaSE 面向对象_第13张图片

接着输出 swap 中 a:1swap 中 b:5

swap 方法执行完毕,释放空间。

JavaSE 面向对象_第14张图片

回到 main 方法,执行剩下的代码,输出 main 中 a:5main 中 b:1,最后 main 方法执行结束,释放了空间,程序结束。

可以看到栈中的局部变量,保存的值是独立的,互不干扰,这两个栈的空间是独立的数据空间。

swap 方法声明的 a 变量与 main 方法声明的 a 变量存储的值互不影响,同理 b 变量也是如此,不要以为 main 方法中的 a 与 b 的值也互换了。


2)在类 B 中:

public void some(int[] arr) {
    arr[0] = 1234;
}
public static void main(String[] args) {
    int[] arr = new int[3];
    new B().some(arr);
    System.out.println(arr[0]);
}

int[] arr = new int[3],在堆中创建了一个 int 类型的一维数组。(我说一下啊,0x??? 是内存地址转化得来的十六进制整数,是实例的唯一标识,每 new 一个实例,就会分配新的空间)

JavaSE 面向对象_第15张图片

随后 arr 作为实参传入并调用 some 方法,开启了新栈。将 main 方法中的 arr 变量保存的值赋给了 some 方法中的 arr 变量,这样有两个引用指向数组实例了。

JavaSE 面向对象_第16张图片

arr[0] = 1234; 将 arr 指向的数组实例的第一个元素保存的值修改为 1234;随后 some 方法栈空间释放。

JavaSE 面向对象_第17张图片

回到 main 方法,输出 arr[0],即 1234。


10.3 类 Person 中,分析输出什么?

1)

int age;
public void some(Person p){
    p = null;
}

public static void main(String[] args) {
    Person p = new Person();
    p.age = 32;

    p.some(p);
    System.out.println(p.age);
}

这里就不画内存图了

main 方法,0x677327b6 {age -> 32},p -> 0x677327b6

调用 some 方法,p -> 0x677327b6,随后 p = null,p -> null。some 方法执行结束,释放空间,回到 main 方法。

输出 p.age,即 32。

别将两个方法中的 p 变量混为一谈,some 中的 p = null,与 main 方法中的 p 互不影响。


2)

int age;

public Person some(Person p) {
    return p;
}

public static void main(String[] args) {
    Person p = new Person();
    p.age = 32;

    Person p1 = p.some(p);
    System.out.println(p == p1);
    System.out.println(p.age == p1.age);
}

main 方法,0x677327b6 {age -> 32},p -> 0x677327b6

some 方法,返回了 p -> 0x677327b6

回到 main 方法,将返回值赋给 p1,p1 -> 0x677327b6

双等号比较的是变量保存的值,这里 p、p1 保存的内存地址相同,都指向 0x677327b6 实例,所以为 true。那么两个引用访问同一个实例变量 age 进行比较,自然也为 true。


3)

int age;

public Person some(Person p) {
    Person p1 = new Person();
    p1.age = p.age;
    return p1;
}

public static void main(String[] args) {
    Person p = new Person();
    p.age = 32;

    Person p1 = p.some(p);
    System.out.println(p == p1);
    System.out.println(p.age == p1.age);
}

main 方法,0x677327b6 {age -> 32},p -> 0x677327b6

some 方法,p -> 0x677327b6,0x14ae5a5 {age -> 32},p1 -> 0x14ae5a5,返回 p1

回到 main 方法,将 0x14ae5a5 赋给 p1,p1 -> 0x14ae5a5,所以 p == p1 为 false,它们指向的不是同一个实例,但两个实例的 age 都是 32,为 true。

4)

Person p = new Person();// p -> 0x677327b6

Person p1 = p;// p1 -> 0x677327b6
p1.age = 2;// 0x677327b6 {age -> 2}
p1 = new Person();// p1 -> 0x14ae5a5
p1.age = 333;// 0x14ae5a5 {age -> 333}

System.out.println(p.age);// 2

10.4 一共有 ?个桃子。每天猴子吃其中的一半加 1 个桃子,当第 10 天时,准备吃,一看,只剩 1 个桃子了,请问 ?的值为?

设第 i 天有 n 个桃子,吃了 n/2 + 1 个桃子,还剩 n - n/2 - 1 个桃子,则第 i+1 天有 n - n/2 - 1 个桃子。

记 n - n/2 - 1 = x,解得 n =(x+1)* 2;

转述为第 i 天有(x+1)* 2 个桃子,第 i+1 天有 x 个桃子。

所以第 i 天的桃子的个数 = (第 i+1 天的桃子的个数+1)* 2。

设计方法求第 i 天的桃子的个数 peach(i),则第 i+1 天的桃子的个数 peach(i+1);

有 peach(i)=(peach(i+1)+ 1) * 2;

当 i = 10 时,桃子个数为 1,peach(10)= 1。

private static int peach(int i) {  
    if (i >= 10) {
        return 1;
    }
    int count = (peach(i + 1) + 1) * 2;
    return count;
}

我们求的 ?是第 1 天的桃子的个数:

public static void main(String[] args) {
    int n = peach(1);
    System.out.println(n);// 1534
}

10.5 有 1 个迷宫由二维数组组成。1 代表墙,0 代表可通行。现有一球在左上角(1,1)处,要求到右下角(7,7),记录小球走过的路径。(注意上下移动的是 x,左右移动的是 y,与坐标系相反)

1 1 1 1 1 1 1 1 1
1 q 0 0 0 0 0 0 1
1 0 1 0 0 0 0 0 1
1 0 1 0 0 0 0 0 1
1 1 1 0 0 0 0 0 1
1 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 1
1 1 1 1 1 1 1 1 1

在 Maze 类中,首先先有一个二维数组作为全局变量;

创建 step 方法记录小球走过的路径,自定义寻路规则:

  • 已经走过的路:死路记为 3,通路记为 2
  • 假设当前坐标为通路记为 2
  • 每一步都先尝试向下走,尝试成功返回 true,否则再尝试右、上,左
int[][] arr;
private boolean step(int x, int y) {
    // 假设当前为通路
    arr[x][y] = 2;
    // 尝试向下、右、上、左,成功返回 true
    if (step(x + 1, y)) {
    	return true;
    } else if (step(x, y + 1)) {
        return true;
    } else if (step(x - 1, y)) {
        return true;
    } else if (step(x, y - 1)) {
        return true;
    }
}
  • 经过以上分支尝试都失败,则返回 false;说明假设错误,不是通路,同时将当前坐标记为 3,代表死路
int[][] arr;
private boolean step(int x, int y) {
    arr[x][y] = 2;
    if (step(x + 1, y)) {
    	return true;
    } else if (step(x, y + 1)) {
        return true;
    } else if (step(x - 1, y)) {
        return true;
    } else if (step(x, y - 1)) {
        return true;
    } else {
        // 尝试失败,标记为死路
        arr[x][y] = 3;
        return false;
    }
}
  • 尝试失败条件之一:遇墙 arr[x][y] = 1,之二:已经走过的路不走,arr[x][y] == 2 或 3

    将失败条件结合,只有当 arr[x][y] = 0 时才能走,否则返回 false

    成功条件,小球走到了终点,即 arr[7][7] = 2

int[][] arr;
private boolean step(int x, int y) {
    if (arr[7][7] == 2) {
        return true;
    }
    if (arr[x][y] != 0) {
        return false;
    }
    arr[x][y] = 2;
    if (step(x + 1, y)) {
    	return true;
    } else if (step(x, y + 1)) {
        return true;
    } else if (step(x - 1, y)) {
        return true;
    } else if (step(x, y - 1)) {
        return true;
    } else {
        arr[x][y] = 3;
        return false;
    }
}
// 构造器初始化二维数组,化为迷宫
public Maze() {
    arr = new int[9][9];
    // 将四周都标记为墙
    for (int i = 0; i < arr.length; i++) {
        // 第一列、最后一列标记为墙
        arr[i][0] = 1;
        arr[i][8] = 1;

        // 第一行、最后一行标记为墙
        arr[0][i] = 1;
        arr[8][i] = 1;
    }

    // 按题目设置墙
    arr[4][0] = 1;
    arr[4][1] = 1;
    arr[4][2] = 1;
    arr[2][2] = 1;
    arr[3][2] = 1;
}
public static void main(String[] args) {
    Maze m = new Maze();
    // 小球的初始坐标
    boolean b = m.step(1, 1);
    System.out.println(b ? "小球找到了路,标记为 2" : "小球迷失了,标记为 3");
    toString(m.arr);
}
public static void toString(int[][] arr) {
    for(int i = 0; i < arr.length; i++) {
        for(int j = 0; j < arr[i].length; j++) {
            System.out.print(arr[i][j] + " ");
        }
        System.out.println();
    }
}

10.6 如图,把 A 中的 n 个圆盘全部移到 C 中。要求:大圆盘不能放在小圆盘上面,每次只能移动一个圆盘,如何移动。

JavaSE 面向对象_第18张图片

先将 n - 1 个小盘从 a 移到 b 处,再将 1 个大盘从 a 移到 c 处,最后将 b 上的 n - 1 个盘移到 c 处。

public static void move(int n, String a, String b, String c) {
    move(n - 1, a, c, b);
    move(1, a, b, c);
    move(n - 1, b, a, c);
}

当只有一个盘时,直接从 a 移到 c。

public static void move(int n, String a, String b, String c) {
    if (n <= 1) {
        System.out.println("将最上面的盘子从 " + a + " 移动到 " + c);
        return;
    }
    move(n - 1, a, c, b);
    move(1, a, b, c);
    move(n - 1, b, a, c);
}

测试一下,比如移动 2 个盘子,从 A 到 C

JavaSE 面向对象_第19张图片

调用方法 move(2, "a", "b", "c") 结果如下:

将最上面的盘子从 a 移动到 b
将最上面的盘子从 a 移动到 c
将最上面的盘子从 b 移动到 c

从 a 移动到 b

JavaSE 面向对象_第20张图片

从 a 移动到 c

从 b 移动到 c

JavaSE 面向对象_第21张图片


10.7 在一个 8*8 格的棋盘摆放 8 个棋子,要求:任意两个棋子之间不能处于同一行、同一列、同一斜线。共有几种摆法?

使用穷举法,第 1 个棋子放在第 1 行的第 1 ~ 8 个,第 2 个棋子放在第 2 行的第 1 ~ 8 个…只不过在放置棋子后,判断它的下标是否与之前的棋子冲突,如果冲突,则将其移一位,然后继续判断…

那么只需要长度为 8 的一维数组就可以保存结果。如 arr[0] 代表第 1 个棋子在第 1 行的下标,arr[1] 代表第 2 个棋子在第 2 行的下标…将其作为成员变量。

判断是否冲突的方法

同一行:arr[i] = arr[j],处于同一斜线上即斜率为 ± \pm ± 1:j - i = ± \pm ±(arr[j] - arr[i]),这里使用 Math.abs 求绝对值省去判断正负的功夫

检查方法需要遍历得到之前的棋子的位置

// 判断第 n + 1 个棋子的位置是否与之前棋子冲突
public boolean isSuitable(int n) {
    for (int i = 0; i < n; i++) {
        if (arr[i] == arr[n] || Math.abs(n - i)
            == Math.abs(arr[n] - arr[i])) {
            return false;
        }
    }
    return true;
}

放置棋子的方法

// n 从 0 开始,放置第 n + 1 个棋子
public void place(int n) {
    for (int i = 0; i < 8; i++) {
        arr[n] = i;
        // 检查第 n + 1 个棋子的位置是否与之前冲突
        if (isSuitable(n)) {
            // 如果没有冲突,则放置下一个棋子
            place(n + 1);
        }
    }
}

当 n = 8 时,说明正准备放置第 9 个棋子,那么前 8 个棋子已摆好,创建方法打印数组查看摆法,同时 count++。

int[] arr = new int[8];
int count = 0;

public void place(int n) {
    if (n >= arr.length) {
        // 摆法又多了一种
		count++;
        print();
        return;
    }
    for (int i = 0; i < arr.length; i++) {
        arr[n] = i;
        if (isSuitable(n)) {
            place(n + 1);
        }
    }
}

public void print() {
    System.out.println("第" + count + "种摆法如下:");
    for(int i = 0; i < arr.length; i++) {
        System.out.println("第" + (i + 1) + "个棋子放在第"
			+ (i + 1) + "行的第" + (arr[i] + 1) + "列");
    }
}

然后创建实例调用 place(0)方法后,访问实例的 count 得到 92,所以一共 92 种摆法。


10.8 设计一个类,完成与电脑的猜拳,电脑随机出石头、布、剪刀,要求显示输赢次数。

类名为 ManMachineFingerGuessingGame;我的想法是这样的,首先一局比赛需要两个参赛者(或人机),设他们的类型为 GuessingBoxer。

class ManMachineFingerGuessingGame {
    private GuessingBoxer b1;
    private GuessingBoxer b2;
}

需要记录他们的姓名、赢的次数、出的招式;

就可以编写一个 throwFinger 方法用于出招,通过 scanner 对象获取输入招式,因为输入字符串容易出错,所以用整数代表招式,0:石头,1:布,2:剪刀;出招后就调用 print 方法就将整数转为招式打印出来。

class GuessingBoxer {
    private String name;
    private int winCount;
    private int finger;
    private static Random random = new Random();

    public GuessingBoxer(String name) {
        this.name = name;
    }

    public int getFinger() {
        return finger;
    }

    public String getName() {
        return name;
    }

    public int getWinCount() {
        return winCount;
    }

    public void addWinCount() {
        winCount++;
    }

    // 出招
    public void throwFinger() {
        Scanner scanner = new Scanner(System.in);
        System.out.print(this.name + "请出招:");
		boolean flag = false;
        
        if (scanner.hasNextInt()) {
            finger = scanner.nextInt();
        } else {
            flag = true;
        }

        if (finger < 0 || finger > 2) {
            flag = true;
        }
        
        if (flag) {
            System.out.print("你不会猜拳,扑该了,我帮你出; ");
            finger = random.nextInt(3);
        }
        print();
    }

    public void throwFinger(boolean isAuto) {
        if (!isAuto) {
            throwFinger();
            return;
        }
        finger = random.nextInt(3);
        print();
    }

    // 根据整数打印招式
    public void print() {
        String move;
        switch (finger) {
            case 0:
                move = "石头";
                break;
            case 1:
                move = "布";
                break;
            case 2:
                move = "剪刀";
                break;
            default:
                System.out.print(this.name + "竖起了中指!");
                return;
        }
        System.out.print(this.name + "出" + move + "; ");
    }
}

好,回到 ManMachineFingerGuessingGame 类,让两个参赛者出招后 vs,谁赢谁的 winCount 就++,记录他的赢的次数;当比赛结束后,调用 statistics 方法打印他们赢的次数,并宣布胜利者。

class ManMachineFingerGuessingGame {
    private GuessingBoxer b1;
    private GuessingBoxer b2;

    public ManMachineFingerGuessingGame(GuessingBoxer b1, GuessingBoxer b2) {
        this.b1 = b1;
        this.b2 = b2;
    }

    public void throwFinger() {
        b1.throwFinger();
        b2.throwFinger(true);

        vs(b1, b2);
    }

    public void vs(GuessingBoxer b1, GuessingBoxer b2) {
        int b1Finger = b1.getFinger();
        int b2Finger = b2.getFinger();

        String winName = b1.getName();
        if (b1Finger == 0 && b2Finger == 2) {
            b1.addWinCount();
        } else if (b1Finger == 1 && b2Finger == 0) {
            b1.addWinCount();
        } else if (b1Finger == 2 && b2Finger == 1) {
            b1.addWinCount();
        } else if (b1Finger == b2Finger) {
            System.out.println("两人平局!");
            return;
        } else {
            b2.addWinCount();
            winName = b2.getName();
        }
        System.out.println(winName + "赢!");
    }

    public void statistics() {
        System.out.print("比赛结束,");
        int c1 = b1.getWinCount();
        int c2 = b2.getWinCount();
        System.out.print(b1.getName() + "赢了 " + c1 +" 场, ");
        System.out.print(b2.getName() + "赢了 " + c2 +" 场, ");
        if (c1 == c2) {
            System.out.println("两人打平。");
        } else if (c1 < c2) {
            System.out.println(b2.getName() + "赢了!!!");
        } else {
            System.out.println(b1.getName() + "赢了!!!");
        }
    }
}

那么入口方法,就需要创建一个比赛场地实例、两个参赛者实例,并设置比赛回合,每次回合调用 throwFinger 方法让参赛者出招后 vs;当所有回合结束,调用 statistics 统计。

class GamePlay {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("请设置你的名字:");
        String b1Name = scanner.next();

        GuessingBoxer b1 = new GuessingBoxer(b1Name);
        GuessingBoxer b2 = new GuessingBoxer("电脑");
        ManMachineFingerGuessingGame game = new ManMachineFingerGuessingGame(b1, b2);

        System.out.print("设置比赛回合:");
        int total = 0;
        if (scanner.hasNextInt()) {
            total = scanner.nextInt();
        } else {
            total = 10;
        }
        System.out.println("比赛一共 " + total + " 回合,招式如下:");
        System.out.println("0:石头,1:布,2:剪刀");
        for (int i = 0; i < total; i++) {
            System.out.println("第 " + (i + 1) + " 回合开始");
            game.throwFinger();
            System.out.println("======================");
        }
        game.statistics();
    }
}

这题受《Think in Java》的作者 Bruce Eckel 的启发稍微改动了下代码:石头剪刀布

你可能感兴趣的:(java,开发语言)