Java 程序设计:有理数类


有了之前 6 篇博客的理论基础,我们来具体编写一个类实践一下。

需求分析

有理数是整数(正整数、0、负整数)和分数的统称,是整数和分数的集合。由于有理数的子集分别是整数和分数,因此对于类的属性的设计,需要提供满足这两种数的表达方式。由于有理数本质上是数,因此有理数类需要实现数能做的事,也就是基本的四则运算。当然可以拓展其他基本运算,例如取绝对值和判断正负号。又由于有理数本质上是数,是数就有大小之分,因此实现有理数的比较非常重要。equals() 方法是每个类都可能需要频繁用到的方法,它是从 Object 类继承下来的方法,尤其是对于大小特征鲜明的数字,该方法也很有必要实现。因为在某些情景下,需要把有理数转换为其他数据类型支持更多操作,因此需要提供数制转换器。最后再加上必要的属性访问器和 toString() 方法,有理数类编写的需求思维导图如下:

设计一个类的一些建议如下:

  1. 保证数据私有,不要破坏封装性;
  2. 数据都要初始化,不应当以来 Java 的默认值;
  3. 不要在类中使用过多的基本类型,而是用其他类来替换;
  4. 并不是所有的属性都需要单独的属性访问器和属性更改器;
  5. 分解有过多职责的类,例如类可以明显地分为两个简单的类;
  6. 类名和方法要尽量体现它们的职责;
  7. 优先使用不可变类,即没有方法可以修改对象状态的类。

类的定义

有理数类应该是一个轮子类,它会被广泛应用于一些需要有理数承载数据的地方。将方法或类声明为 final 可以确保其不会在子类中改变语义,例如 String 类这种不可变类就要注意避免这种事发生,Rational 类同理。因为我们希望更方便地进行有理数的大小比较,并且希望兼容 Arrays 的 sort() 方法进行排序,因此此处用 implements 关键字启用 Comparable 接口。

public final class Rational implements Comparable<Rational>

类的属性

根据需求,设计类的属性时需要考虑整数和分数。不过整数也可以认为是分数的子集,因为整数可以看作是分母为 1 的分数。因此这里只需要 2 个属性分别表示分子和分母,不需要更多的属性或者状态变量来进行管理。
为了保证数据私有,不要破坏封装性,此处将所有类的属性的访问修饰符都设置为 private。同时我选择将实例属性定义为 final 属性,这样的属性必须在构造对象是初始化,并且之后不能再修改。final 修饰符对于类型为基本类型或者不可变类属性很有用,例如 String 类就是个不可变类,而有理数类是类似于 String 类起到轮子功能的类。

private final int numerator;
private final int denominator;

我们考虑一个重要的问题,用户永远无法输入无限不循环小数,我们是否需要考虑接收用户的小数呢(有限位数的小数是有理数)?无论数学上还是理论上可行,此处我的个人建议是绝对不要这么做。做到这一点并不难,比如说把分子改为是诸如 double 类型的属性,然后分母统一设置为 1。用户没办法表示一个无限不循环小数,因为用户输入的小数始终会是有限个的,不过用户也可以用表达式注入参数,此时我们无法保证某个表达式的计算结果不会是无理数。
实践经验告诉我们,永远不要相信所有用户,不要天真地希望他们会按照开发者的心意来使用工具。如果我们这里开放了有理数的小数表示,则会留下别有用心或者胡来的用户注入无理数等奇怪的东西,从而使得我们的“有理数类”逻辑不清,定语形同虚设,这个类就是个失败的类。所以这里我们就把属性写死了,不允许任何浮点数被输入。

构造方法

由于用户输入的数字可能是整数或小数,因此我们需要重载一些类的构造方法。

Rational(int num) 方法

该方法对应的是最简单的情况,即用户输入的是个整数,整数一定是有理数了。

//输入整数的构造方法
public Rational(int num) {
      this.numerator = num;
      this.denominator = 1;
}

Rational(int numerator, int denominator) 方法

该方法对应的是分数形式的有理数,方法传入该有理数的分子和分母,分别将其赋值给对应属性。注意此时传入的分母不能为 0,否则这是个不合法的输入,应该主动抛出一个异常告知类的使用者。

//输入分子和分母的构造方法
public Rational(int numerator, int denominator) throws IllegalArgumentException{	
      if(denominator == 0) {    //不合法有理数判断,分母不为 0
            throw new IllegalArgumentException("分母是个不合法的参数");
      }
		
      int gcd = getGCD(numerator, denominator);    //取分子和分母的最大公约数,用于化简
      this.numerator = numerator / gcd;
      this.denominator = denominator / gcd;
}

注意到我们使用了 throws 关键字进行异常规范声明,展示这个方法可能抛出的异常。我们选择抛出的异常是 IllegalArgumentException 异常,这个是 Exception 类分支下的不合法的参数异常类

Rational(String str) 方法

这是最令人头疼而难办的方法,因为类通常会重载一个支持用字符串进行初始化的构造方法,以满足不同的需求(例如从文件流中读入数据创建对象)。但是参数是字符串意味着用户可以将任何内容以字符串的形式注入构造方法,上面的需求写到了,我们定义的 Rational 类仅支持整数形式和分数形式 2 种表示方法。也就是说我们需要判断字符串是否是我们所期望的输入形式,如果是其他恶意的输入,我们的方法应当主动地过滤掉并抛出异常。

//输入字符串的构造方法
public Rational(String str) throws IllegalArgumentException{
		
      int idx = 0;
      if(isInteger(str)){    //检测是否是整数型输入
            this.numerator = Integer.valueOf(str);
            this.denominator = 1;
      }
      else if(str.indexOf("/") != -1){    //检测是否是分数型输入
            String numerator_str = str.substring(0, str.indexOf("/"));
            String denominator_str = str.substring(str.indexOf("/") + 1, str.length());
			
            if(isInteger(numerator_str) && isInteger(denominator_str)){    //判断除号两边是否是整数
	          int numerator = Integer.valueOf(numerator_str);
		  int denominator = Integer.valueOf(denominator_str);
		  if(denominator == 0) {
		        throw new IllegalArgumentException("分母是个不合法的参数");
		  }
				
		  int gcd = getGCD(numerator, denominator);
		  this.numerator = numerator / gcd;
		  this.denominator = denominator / gcd;
	    }
	    else {    //除号两遍不是整数
	          throw new IllegalArgumentException("输入的字符串是不合法的参数");
	    }
      }
      else {    //不合法输入
            throw new IllegalArgumentException("输入的字符串是不合法的参数");
      }	
		
}

仔细阅读代码,我们首先使用 isInteger() 方法判断传入的字符串是否是整数,如果是则将字符串转换成整数后进行实例化。如果不是整数,则进入下一步的判断,即判断传入的字符串是否是以分数形式出现。若字符串中有除号 “/”,则使用 isInteger() 方法进一步判断除号两侧的字符串是否是整数。如果这 2 个验证都通过了,就按照分数形式进行实例化,否则就应该抛出一个 IllegalArgumentException 异常。

辅助方法

getGCD(int numerator, int denominator) 方法

该方法用于取分子和分母的最大公约数,用于给分数进行化简,使用的是大家熟知的辗转相除法。需要注意的是分子和分母的大小关系不一定,因此进入算法之前应该用个分支判断哪个是大数,不过这个应该是编程的初学者才会欠考虑的地方。
注意到我在返回最大公约数之前,单独放了一个分支结构来进行符号逆转。因为我们不能够相信用户的任何输入,表示一个分数是正数还是负数,可以是分子为负数,也可以是分母为负数。有的用户可能会令分子、分母都为负数,来表示这个分数是正数。因此我们为了后面的方法编写便捷,并且起到一个标准化的作用,统一要求表示负数只能在分子处表示。将负数的状态转移到分子是很简单的,无论分子状态如何,只要分母是负数就给最大公约数取反,当最大公约数返回构造方法时就能够实现。

//求 2 个整数的最大公约数,用于分数化简
private int getGCD(int numerator, int denominator){
      int num1;
      int num2;
      //保证是大数除小数
      if(numerator > denominator) {	
            num1 = Math.abs(numerator);
	    num2 = Math.abs(denominator);
      }
      else {
	    num1 = Math.abs(denominator);
	    num2 = Math.abs(numerator);
      }
		
      int remainder = num1 % num2;
      while (remainder != 0) {
            num1 = num2;
            num2 = remainder;
            remainder = num1 % num2;
      }
      //逆转负号,把分母的负号转移到分子上
      if(denominator < 0) {
            num2 = -num2;
      }
      return num2;
}

照我这么说,那使用连续嵌套的负号和括号表示负数怎么办?这个我们不需要担心,因为构造方法会认为这是非法输入。还是那句话,与其在后面的方法考虑所有的情况,还不如在一开始就提供一个标准。后面的方法就不要考虑这些复杂的情况了,而应当专注到方法的本职工作上。

isInteger(String str) 方法

该方法就是上述代码中用于判断字符串是否是整数的方法,它使用正则表达式实现。Java 要启用正则表达式之前,需要先把 Pattern 包含进来。

import java.util.regex.Pattern;

我们先造一个 Pattern 对象来编写正则表达式,Pattern 对象是一个正则表达式的编译表示,compile() 方法用于编译正则表达式。Matcher 对象是对输入字符串进行解释和匹配操作的引擎,我们需要调用 Pattern 对象的 matcher() 方法来获得一个 Matcher 对象。最后使用 matches() 方法,尝试将整个区域与模式匹配。

//判断是否是整数
private static boolean isInteger(String str) {  
      Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");  
      return pattern.matcher(str).matches();  
}

注意到这里我使用 static 关键字,将 isInteger(String str) 定义为静态方法。当方法被定义为静态方法时,该方法不在对象上执行(或者说没有 this 参数),例如 Math 类的 pow 方法,该方法实现幂运算是不需要 Math 对象。当方法不需要访问对象状态,或方法只需要访问类的静态字段时,可以将方法定义为静态方法。这里定义成静态方法的好处是正则表达式只需要编译一次,就可以供整个类来使用,否则每次调用方法都编译一次正则表达式,是一笔较大的开销。

基本运算方法

四则运算

有理数类肯定要支持基本的四则运算啦,实现的思路也很简单。因为我们把整数当做是分母为 1 的小数,因此所有四则运算的方法都可以按照小数的计算思路来实现。方法将会返回一个新的 Rational 对象,由于调用构造方法时会自动对分数进行化简,因此我们不需要对计算结果进行多余的化简操作。同样一个 Rational 对象能够创建,其分母一定不为 0,这里无需对除零进行多余的判断,也自然不需要抛出多余的异常。

//加法运算
public Rational add(Rational num2) {
      int numerator = this.numerator * num2.denominator + denominator * num2.numerator;
      int denominator = this.denominator * num2.denominator;
      return new Rational(numerator, denominator);
}
//减法运算
public Rational subtract(Rational num2) {
      int numerator = this.numerator * num2.denominator - denominator * num2.numerator;
      int denominator = this.denominator * num2.denominator;
      return new Rational(numerator, denominator);
}
//乘法运算
public Rational multply(Rational num2) {
      int numerator = this.numerator * num2.numerator;
      int denominator = this.denominator * num2.denominator;
      return new Rational(numerator, denominator);
}
//除法运算
public Rational divide(Rational num2) {
      int numerator = this.numerator * num2.denominator;
      int denominator = this.denominator * num2.numerator;
      return new Rational(numerator, denominator);
}

总之思路还是一样的,一开始就应该把该做的过滤操作做好,不要放任何可能出现问题的数据进到其他方法。执行基本运算的方法仅负责进行基本运算,由其他方法完成的任务,就不要让这些方法多做考虑。

绝对值和正负性

也挺好写的,请先回顾一下我们在 getGCD(int numerator, int denominator) 方法中已经把有理数如何表达正负搞好了,因此这里我们才能写出这么简单的代码。如果没有对分母的正负性加以统一,这里 2 种操作都要尽可能考虑到不同的可能。还是一样,一开始就应该把该做的过滤操作做好,不要放任何可能出现问题的数据进到其他方法。

//返回绝对值
public Rational abs() {
      if(this.numerator < 0) {
            return new Rational(-1 * this.getNumerator(),this.getDenominator());
      }
      else {
            return new Rational(this.getNumerator(),this.getDenominator());
      }
}
//判断有理数正负
public boolean judgePlusMinus(){
      if(this.getNumerator() < 0) {
            return false;
      }
      else {
            return true;
      }
}

实现 Comparable 接口

我们编写的是有理数类,有理数是数字,自然有大小之分。我们希望能够用合适的方式比较有理数类对象的大小,同时我们还希望有理数类可以兼容 sort() 方法享受快排的快感。 Arrays 类中的 sort 方法可以对对象数组进行排序,但是 sort 应该按照什么规则进行排序呢?这个时候就可以使用 Comparable 接口,这个接口能够用于描述某个类比较大小的规则。

compareTo(Rational num2) 方法

接口用于描述类应该做什么,而不是指定类具体要怎么实现,一个类中可以实现多个接口。在有些情况下,我们的需求符合这些接口的描述,就可以使用实现这个接口的类的对象。一旦类启用了接口,就必须要实现接口中的所有方法,Comparable 接口中仅有 compareTo() 方法,这就是 sort() 方法比较大小的依据。

//实现 Comparable 接口的所有方法
public int compareTo(Rational num2) {
      int result = this.subtract(num2).getNumerator();
      if(result > 0){
            result = 1;
      }
      else if(result < 0) {
            result = -1;
      }
      return result;
}

取最大/最小

除了通过 Comparable 接口实现对其他容器比较大小的兼容,对 2 个有理数对象进行比较的方法也可能很常用。这里我们利用 compareTo() 方法,可以很轻松地完成对 2 个有理数对象取最值的方法编写。

//取 2 个有理数中的最大值
public static Rational max(Rational num2) {
      if(this.compareTo(num2) > 0) {
            return this;
      }
      else {
            return num2;
      }
}
//取 2 个有理数中的最小值
public static Rational min(Rational num2) {
      if(this.compareTo(num2) < 0) {
            return this;
      }
      else {
            return num2;
      }
}

equals() 方法

Object 类是 Java 中所有类的超类,Java 中的每个类都是 Object 类的子类。equals() 方法可以检测一个对象是否等于另一个对象,在 Object 类中的判断方式是判断 2 个对象的引用是否相等。但是在很多情况下我们不关心他们的引用是否相等,而是关系各个类的属性是否相等。因此在基础 equals 方法时,我们经常需要针对状态来进行覆盖。设计 equals() 方法时可以参考以下几点:

  1. 显式参数命名为 otherObject,比较时用强制类型转换为另一个 other 变量;
  2. 检测 this 和 otherObject 是否相等;
  3. 检测 otherObject 是否是 null;
  4. 比较 this 与 otherObject 的类是否相同;
  5. 将 otherObject 强制类型转换为相应的类类型变量,比较各个属性。
public boolean equals(Object obj) {
      //检测 this 和 otherObject 是否相等
      if(this == obj) {
            return true;
      }
      //检测 otherObject 是否是 null
      if(obj == null){
            return false;
      }
      //比较 this 与 otherObject 的类是否相同
      if(this.getClass() != obj.getClass()) {
            return false;
      }
      //强制类型转换为相应的类类型变量,比较各个属性
      Rational num2 = (Rational) obj;
      return this.numerator == num2.numerator
          && this.denominator == num2.denominator;
}

数制转换

有的时候我们需要把有理数转换为其他数据类型,以此支持更多的操作,思路很简单,用强制类型转换即可。

//有理数转化为 int 类型
public int intValue() {
      return (int) this.numerator / this.denominator;
}
//有理数转化为 double 类型
public double doubleValue(){
      return (double) this.numerator / this.denominator;
}

属性访问器和 toString() 方法

注意到我们把所有的属性统统设为了 final,因此这里不需要属性修改器。

public int getNumerator() {
      return numerator;
}

public int getDenominator() {
      return denominator;
}

public String toString() {
      if(this.denominator == 1) {
            return "The Rational is " + this.numerator;
      }
      else {
            return "The Rational is " + this.numerator + "/" + this.denominator;
      }
}

类的测试

测试一个类的功能是否正常,需要注入错误的参数进行报错参数,看看类是否能够过滤错误的参数。也要输入具有不同特征的测试数据,对类的各个方法逐一测试。

Rational 类代码

点我下载

错误输入测试

故意输入错误的参数,这 2 种测试代码都应该抛出异常。

public static void main(String[] args) {
      Rational num1 = new Rational("我是恶/意注入");
      System.out.println(num1.toString());
}
public static void main(String[] args) {
      Rational num1 = new Rational("我不是有理数");
      System.out.println(num1.toString());
}
public static void main(String[] args) {
      Rational num1 = new Rational("1/0");
      System.out.println(num1.toString());
}

类功能测试

public static void main(String[] args) {
	//构造方法测试
	Rational array[] = new Rational[11];
	array[0] = new Rational(0);
	array[1] = new Rational(1);
	array[2] = new Rational("2");
	array[3] = new Rational("6/2");
	array[4] = new Rational(4,1);
	array[5] = new Rational(1,2);
	array[6] = new Rational("1/3");
	array[7] = new Rational(2,5);
	array[8] = new Rational("5/8");
	array[9] = new Rational(-1,2);
	array[10] = new Rational("3/-5");
	for(int i = 0; i < array.length; i++) {
		System.out.println("array[" + i + "]:" + array[i].toString());
	}
	
	//加法运算测试
	System.out.println("加法测试1:" + array[0].add(array[1]));
	System.out.println("加法测试2:" + array[2].add(array[5]));
	//减法运算测试
	System.out.println("减法测试1:" + array[3].subtract(array[4]));
	System.out.println("减法测试2:" + array[2].subtract(array[6]));
	//乘法运算测试
	System.out.println("乘法测试1:" + array[2].multply(array[3]));
	System.out.println("乘法测试2:" + array[4].multply(array[6]));
	//除法运算测试
	System.out.println("除法测试1:" + array[4].divide(array[2]));
	System.out.println("除法测试2:" + array[3].divide(array[8]));
	//绝对值运算测试
	System.out.println("绝对值测试1:" + array[7].abs().toString());
	System.out.println("绝对值测试2:" + array[9].abs().toString());
	//有理数正负性判断测试
	System.out.println("正负性判断测试1:" + array[6].judgePlusMinus());
	System.out.println("正负性判断测试2:" + array[10].judgePlusMinus());
	
	//Comparable 接口测试
	Arrays.sort(array);
	for(int i = 0; i < array.length; i++) {
		System.out.println("array[" + i + "]:" + array[i].toString());
	}
	System.out.println("最大值测试1:" + array[1].max(array[2]));
	System.out.println("最大值测试2:" + array[4].max(array[3]));
	System.out.println("最小值测试1:" + array[5].max(array[6]));
	System.out.println("最小值测试2:" + array[8].max(array[7]));
	
	//相等判断测试
	Double test_num1 = null;
	Integer test_num2 = new Integer(1);
	Rational test_num3 = new Rational(0);
	System.out.println("相等测试1:" + test_num3.equals(test_num1));
	System.out.println("相等测试2:" + test_num3.equals(test_num2));
	System.out.println("相等测试3:" + test_num3.equals(array[2]));
	System.out.println("相等测试4:" + test_num3.equals(array[1]));
	
	//数制转换测试
	System.out.println("数制转换测试1:" + array[6].doubleValue());
	System.out.println("数制转换测试2:" + array[6].intValue());
}

构造方法测试

合格的构造方法应该支持 3 种不同的初始化方式,并且能够实现分数的化简和负数表示的统一。

同时对于分母为 0 的输入,构造方法应当有能力过滤掉。

基本运算测试

基本运算的方法的返回结果,应该符合数学逻辑。

Comparable 接口测试

Rational 类应该支持 Arrays 的 sort() 方法,并且能够正确回显最值。

相等判断测试

equals() 方法需要判断出不同的类型,并且对相同的类型比较属性。

数制转换测试

方法应当正确地将有理数转换为目的类型。

问题讨论

问题一

描述与 C 语言的有理数代码相比较,为什么你设计的类更加面向对象?

这个问题我认为问法不是很好,为了讲清这个问题,我们还是得先回顾下为什么需要面向对象编程。由于 C 语言是面向过程的语言,因此我们使用 C 语言只能实现结构化程序设计。所谓结构化程序设计就是“程序 = 数据结构 + 算法”,而在编写有理数结构时程序中会有很多可以相互调用的函数和变量。可以显然地看出,这种编程风格存在不少缺点。首先由于函数之间可以相互调用,任何函数都可以修改全局变量,这就导致了函数、数据结构之间的关系一段乱麻,尤其是当代码量很长的时候,代码的理解也变得极其困难。

  • 这个函数是用来操作哪个还是哪些数据数据结构?
  • 这个数据结构可以被哪些函数操作,代表什么含义?
  • 不同的函数能够相互调用吗?关系是什么?

对于变量来说,有的变量可能是只能被一些函数修改,而不能被某些函数操作,但是由于你不能给变量“上锁”,因此这个变量会很轻松地被修改。当然你可以说可以搞成常量,那我如果要修改呢?搞成常引用?算了吧,这样变量间的关系就跟复杂了。而且,某个数据结构会被多个函数调用,而如果出现了错误,是哪个函数出错了呢?是函数一、函数二、函数 N,还是都有错?除了调试好像还真没啥好办法。同时作为一个懒人,如果我的一个程序的某个功能我曾经写过,那么我就很喜欢去把以前的代码搞来用。可是往往这会是一件困难的事情,因为这段代码的源程序的变量和函数间本身就有复杂的逻辑关系,接口可能完全不一样,最后你发现还不如重写一遍。

简单地说,面向对象程序设计就是把某个客观事物的共同特点归纳出来,形成一个数据结构。对象既表示客观世界问题空间中的某个具体事物,有表示软件系统解空间中的基本元素。在面向对象的编程风格中“对象 = 属性 + 方法”,对象中既包含属性(数据),也包含方法(代码),是某一类具体事物的特殊实例。此处我们将有理数这个对于计算机来说抽象的概念,利用代码把它具体化为一种对象,这种对象具有分子和分母 2 个状态需要记录。同时这个对象与特定的动作绑定在一起,这些动作都是这个对象独有的具有的可发生的操作。而面向过程的编程风格不仅没有封装性,更不能在语法上将对象和方法绑定在一起,函数和变量的关系真的只能靠开发者自觉遵守。

问题二

别人如何复用你的代码?

Java 提供了非常好用的“包 (package)”机制来实现代码的复用,包能够很好地组织类,同时也用于区别类名的命名空间。包把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。基于这种机制,开发者可以自己把一组类和接口等打包,让其他开发者复用,同时这也能帮助其他开发者更容易地确定哪些类、接口、枚举和注释等是相关的。使用包中的某个类时,其他开发者可以使用 import 把类导入进来用。
同时包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类,这也使得其他程序员复用代码时不能瞎用、瞎继承我们的代码。

问题三

别人的代码是否依赖你的有理数类的属性?当你的有理数类的属性修改时,是否会影响他人调用你有理数类的代码?

这个问题的表述我觉得有点含糊,我按照我对题目的理解来讨论。当其他开发者在他们的代码上复用了有理数类,并且实例化出对象来使用时,这些实例化出来的对象与我无瓜。因为类是一种抽象模型,是一种概念性的存在,实例化对象的属性时就需要根据属性的要求填充数据。当类实例化赋予了具体的状态之后,这些实例化的类就会成为彼此独立而有用的个体。我和其他开发者可能都会大范围使用到有理数类,但是使用的情景可能完全不相同,而实例化的对象没有任何关系,因为他们是独立的个体。
所以别人的代码依赖我的有理数类的属性,因为这些属性是我给这个对象抽象出来的状态,因为有了这些状态我们才能具体地描述这些对象。如果我修改了有理数的属性,那么其他开发者调用时就得跟着用我修改的内容。但是如果对于实例化之后的对象,对象彼此之间是独立的个体,不会对代码调用产生影响。

问题四

有理数类的 public 方法是否设置合适?为什么有的方法设置为 private?

public 定义的方法表示任何类的任何方法都可以调用,不过有时候可能会破坏封装性。对于方法来说,一个方法的实现可以被拆分出几个辅助方法,这些辅助方法对于外界来说不应该成为公共接口的一部分。例如上述我写的代码,在构造方法的编写时就使用了 getGCD(int numerator, int denominator) 和 isInteger(String str) 两个辅助方法。此时我使用 private 来定义,使得方法被定义为私有方法。这种做法的好处在于当你原来的方法重新设计而不需要辅助方法时,由于是私有方法,你不用担心这个方法会被其他程序所需要,可以直接删除。如果它是共有地的方法,即使你本人不再使用,也不能确定在其他开发者的程序中也不再使用。

参考资料

《Java 核心技术 卷Ⅰ》,[美]Cay S.Horstmann 著,林琪 苏钰涵 等译,机械工业出版社
Java 面向对象:对象、字段和方法
Java 面向对象:类的继承
Java 面向对象:Object 类
Java 面向对象:接口
Java 异常类与捕获异常
Python 面向对象编程
Java 正则表达式
Java 包(package)
Java中判断字符串是否为数字的五种方法

posted @ 2020-10-04 06:30  乌漆WhiteMoon  阅读(1250)  评论(8编辑  收藏  举报