Java程序设计实验3 | 面向对象(上)(一)+ https://developer.aliyun.com/article/1520470?spm=a2c6h.13148508.setting.14.de6d4f0e3Ph7xG
4、设计一个分数类
分数的分子和分母用两个整型数表示,类所拥有的方法包括对分数进行加、减、乘、除等运算,以及输出分数的方法,输出分数的格式应该是:分子/分母。
在测试类中定义分数类对象,运算并输出运算结果。
本题若不考虑分数的约分,难度会小一些。在该题的拓展部分介绍了约分的方式。
整体思路如下面源代码所示:
- 定义一个Fraction分数类,其中包含mol(分子,molecule)和den(分母,denominator)两个成员变量。
- 重写toString()方法,指定以 mol+"/"+den 的格式输出。
- 通过含参构造方法实例化分数类。注意分母不能为0的限定条件。若分母为0,在构造函数中抛出异常。
- 设计加减乘除四则运算。注意这里的思路。以分数加法的方法为例:
public Fraction add(Fraction x){ return new Fraction(this.mol*x.den + x.mol*this.den, this.den*x.den); }
两个分数运算后得到的结果仍然是分数。因此,该成员方法的返回值为Fraction,传入的运算参数类型也为Fraction。确定了方法的返回值与参数后,那么问题就来了:如何让当前分数对象与传入的分数对象x做运算呢?
有些同学可能会想到一个思路,就是求出当前对象的分数值后再直接与x相加。即:
this.mol/this.den + x; //Error
但这种做法是完全行不通的。首先这一定会遇到类型转换的问题:this.mol/this.den是int类型,而x是Fraction类型,二者之间不能进行运算。这时又有人想到,可以通过构造方法把 this.mol/this.den 以似乎是Fraction类型的形式表现出来,即:
new Fraction(this.mol,this.den) + x; //Error
但事实上,这也是完全错误的。Fraction是引用类型,两个引用类型之间怎么能用+号进行运算呢?
出现这两种错误思路的根本原因在于:没有意识到到底是谁在进行运算。两个引用类型之间能直接进行四则运算吗?当然是不能的。而是引用类型的成员之间进行四则运算。就和两个引用类型进行比较一样,它们不能用大于号小于号等于号直接比较,而要用equals或Comparable接口实现compareTo()接口后,按照某种指定的规则进行比较。
同样地,当我们说两个分数进行相加,并不是“分数”这个概念进行的运算,而是分子和分母之间进行的运算。正确的做法是:通过构造方法将分子相加的结果、分母相加的结果得到一个新的分数,这个分数才是最终的结果。即:
new Fraction(this.mol*x.den + x.mol*this.den, this.den*x.den); //括号中的内容即通分相加的过程
其余的三则运算同理。一个引用类型与它的成员是密不可分的。当要对某一个引用类型进行操作时,要顺带考虑一下是否关系到它的成员。
源代码:
class Fraction { private int mol; //分子 private int den; //分母 public Fraction(int x, int y) { if (y == 0){ throw new ArithmeticException("分母为0!"); } this.mol = x; this.den = y; } public Fraction add(Fraction x){ return new Fraction(this.mol * x.den + x.mol * this.den,this.den * x.den); } public Fraction sub(Fraction x){ return new Fraction(this.mol * x.den - x.mol * this.den,this.den * x.den); } public Fraction mul(Fraction x){ return new Fraction(this.mol * x.mol,this.den * x.den); } public Fraction div(Fraction x){ if(x.mol == 0) { throw new ArithmeticException("除数为0!"); } return new Fraction(this.mol * x.den,this.den * x.mol); } @Override public String toString() { return mol+"/"+den; } }
public class S3_4 { public static void main(String[] args) { Fraction fraction1 = new Fraction(4,5); Fraction fraction2 = new Fraction(3,4); System.out.println("分数1:" + fraction1 + " 分数2:" + fraction2); System.out.println("add:" + fraction1.add(fraction2)); System.out.println("sub:" + fraction1.sub(fraction2)); System.out.println("mul:" + fraction1.mul(fraction2)); System.out.println("div:" + fraction1.div(fraction2)); } }
列出测试数据和实验结果截图:
令fraction1的分母为0,则:
令fraction2的分子为0(即令fraction2为0),则除数为零,则:
⭐拓展:分数约分
分数约分的本质是找到分子mol与分母den的最大公约数。分子与分母同时除以该最大公约数的过程就是约分。本文在此拓展求最大公约数的方法:辗转相除法。
内容扩展:
迭代版
import java.util.Scanner; public class Test { public static void main(String[] args) { //辗转相除法求最大公约数、最小公倍数 Scanner reader = new Scanner(System.in); //输入两个数 int n = reader.nextInt(); int m = reader.nextInt(); /*因为求最大公约数和最小公倍数都需要用到m、n,且辗转相除的过程会改变n、m的值, 故再创建两个变量n2、m2,把m和n的值拷贝一份再做运算*/ int m2 = m; int n2 = n; int r = n2 % m2; //最大公约数 while (r != 0) { n2 = m2; m2 = r; //注意:m2才是所求的最大公约数的结果,而不是r r = n2 % m2; } System.out.println(m2); } }
递归版
public static int getComDivisor(int a , int b){ if(a % b == 0) { return b; } return getComDivisor(b,a % b); }
public static void main(String[] args) { Scanner reader = new Scanner(System.in); //输入两个数 int n = reader.nextInt(); int m = reader.nextInt(); System.out.println(getComDivisor(n,m)); }
运用到该题
源代码:
public class S3_4 { public static void main(String[] args) { Fraction fraction1 = new Fraction(4,5); Fraction fraction2 = new Fraction(3,4); System.out.println("分数1:" + fraction1 + " 分数2:" + fraction2); System.out.println("add:" + fraction1.add(fraction2)); System.out.println("sub:" + fraction1.sub(fraction2)); System.out.println("mul:" + fraction1.mul(fraction2)); System.out.println("div:" + fraction1.div(fraction2)); } } class Fraction { private int mol; //分子 private int den; //分母 //新增 private int getComDivisor(int a , int b){ if(a % b == 0) { return b; } return getComDivisor(b,a % b); } public Fraction(int x, int y) { if (y == 0){ throw new ArithmeticException("分母为0!"); } this.mol = x; this.den = y; int tmp = getComDivisor(x,y); this.mol = x/tmp; this.den = y/tmp; } // public Fraction add(Fraction x){ return new Fraction(this.mol * x.den + x.mol * this.den,this.den * x.den); } public Fraction sub(Fraction x){ return new Fraction(this.mol * x.den - x.mol * this.den,this.den * x.den); } public Fraction mul(Fraction x){ return new Fraction(this.mol * x.mol,this.den * x.den); } public Fraction div(Fraction x){ if(x.mol == 0) { throw new ArithmeticException("除数为0!"); } return new Fraction(this.mol * x.den,this.den * x.mol); } @Override public String toString() { return mol+"/"+den; } }
列出测试数据和实验结果截图:
5、设计一个雇员类
(1)属性包括:编号、姓名、年龄、职务、部门,要求合理选定属性类型;该雇员类还拥有统计出勤人数的功能,可以考虑为雇员类设计一个静态属性;方法包括:构造方法、输出信息的方法、签到方法;
(2)创建雇员类对象,统计雇员的出勤人数。注意考虑属性和方法的访问权限,方法的功能,及main方法中如何实现要求统计的信息。
本题的思路如下:
1、成员属性编号、姓名、年龄、职务、部门封装,用private修饰,设计setter与getter方法。
2、静态属性attendance用于统计雇员出勤人数,用static修饰。默认初始化为0.
3、输出信息的方法通过重写toString()方法完成。
4、签到方法的功能是提示签到成功,并令attendance++,以达到统计出勤人数的目标。
源代码:
public class S3_5 { public static void main(String[] args) { //实例化雇员对象 Employee employee1 = new Employee(1001,"虹猫",20,"产品经理","开发部"); Employee employee2 = new Employee(1002,"蓝兔",19,"食堂队长","后勤部"); Employee employee3 = new Employee(1003,"大奔",21,"技术总监","技术部"); Employee employee4 = new Employee(1004,"莎莉",18,"销售总监","销售部"); //签到 Employee[] employees = {employee1,employee2,employee3,employee4}; for (Employee x : employees) { System.out.println(x.toString()); Employee.signIn(x); } //公布出勤人数 System.out.println("共" + Employee.attendance + "人出勤!"); } } class Employee{ //出勤人数,默认初始化为0 public static int attendance; //成员属性 private int ID; private String name; private int age; private String post; private String department; //getter与setter public int getID() { return ID; } public void setID(int ID) { this.ID = ID; } 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 String getPost() { return post; } public void setPost(String post) { this.post = post; } public String getDepartment() { return department; } public void setDepartment(String department) { this.department = department; } //构造方法 public Employee(int ID, String name, int age, String post, String department) { this.ID = ID; this.name = name; this.age = age; this.post = post; this.department = department; } //输出信息的方法:重写toString() @Override public String toString() { return "ID=" + ID + ", name='" + name + '\'' + ", age=" + age + ", post='" + post + '\'' + ", department='" + department + '\'' + '}'; } //签到方法 public static void signIn(Employee x) { System.out.println(x.name + "签到成功!"); Employee.attendance++; //用于统计 } }
列出测试数据和实验结果截图:
6、设计一个电视机类
属性包括商品编号、开关状态、音量、频道等,同时设计一些方法对电视机的状态进行控制。例如,方法应包括开/关电视机、更换频道、提高/减小音量等。要求商品编号自动生成(可以考虑为电视机类设置一个管理商品编号的静态成员变量,或者专门设置一个编号管理类)。
注意:有些成员变量需要定义为静态的(static),控制和操纵静态成员变量的方法应是静态的(static)。
本题思路如下:
1、设计静态变量num用于自动生成产品编号。每次构造一个电视机实例,num值加一。将num值赋给电视机编号itemNO即可实现自动生成。
2、开关电视机的成员方法tvSwitch()。用boolean变量status控制电视机的开关。该方法用于改变status的值,若为true,则改为false;若为false则改为true,以此实现电视机的开关。
3、调节音量的实现方式是令音量值向上增1或向下减1.要向上调节音量还是向下调节取决于用户传入的adjust变量值为true还是false。true则向上调节,false则向下调节。音量的调节范围是0~100.对临界情况的调节要予以考虑。
4、调节频道有两个重载的方法。可以通过和音量一样的调节方式调节,也可以由用户指定频道,直接跳转到目标频道。频道的调节范围是0~100.对临界情况的调节要予以考虑。
5、注意:在对电视机进行操作之前,必须确保电视机是开机状态,即status为true。否则不能进行相关操作。
6、main方法中设计了4个不同状态的电视机对象,对Television类进行测试。最终输出四个对象的相关信息,通过重写toString方法实现。
源代码:
public class S3_6 { public static void main(String[] args) { Television tv1 = new Television(); Television tv2 = new Television(); Television tv3 = new Television(); Television tv4 = new Television(); //打开tv1、tv2、tv3 tv1.tvSwitch(); tv2.tvSwitch(); tv3.tvSwitch(); System.out.println("电视机tv1、tv2、tv3已打开!"); //尝试将tv1的音量上调(true)、tv2的音量下调(false) System.out.println("==========尝试将tv1的音量上调(true)、tv2的音量下调(false)=========="); tv1.adjustVolume(true); tv2.adjustVolume(false); //尝试将tv3的频道换成50、将tv1的频道换成100 System.out.println("==========尝试将tv3的频道换成50、将tv1的频道换成100=========="); tv3.changeChannel(50); tv1.changeChannel(100); //尝试将tv3向下调台、将tv1向上调台 System.out.println("==========尝试将tv3向下调台、将tv1向上调台=========="); tv3.changeChannel(false); tv1.changeChannel(true); //获取tv2当前的频道和音量 System.out.println("==========获取tv2当前的频道和音量=========="); System.out.println(tv2.getVolume()); System.out.println(tv2.getChannel()); //对未开机的tv4进行操作时: System.out.println("==========对未开机的tv4进行操作=========="); tv4.changeChannel(50); tv4.adjustVolume(true); //输出各个电视机的信息 System.out.println("==========输出各个电视机的信息=========="); System.out.println(tv1.toString()); System.out.println(tv2.toString()); System.out.println(tv3.toString()); System.out.println(tv4.toString()); } } //电视机类 class Television{ private static int num; //管理商品编号的静态成员变量 private String itemNO; //商品编号 private boolean status; //开关状态 private int volume; //音量 private int channel; //频道 public int getVolume() { if (this.status) { return volume; }else { return -1; } } public boolean isStatus() { return status; } public int getChannel() { if (this.status) { return this.channel; }else { return -1; } } //构造方法,自动生成产品编号 public Television() { Television.num++; this.itemNO = "tv000" + Television.num; } //开关电视机 public boolean tvSwitch() { if(!this.status) { this.status = true; } else { this.status = false; } return this.status; } //上调或下调频道 public void changeChannel(boolean change){ if(change) { this.channel++; if(channel > 100) { System.out.println("已达最大频道,无法增加!"); this.channel--; } }else{ this.channel--; if(channel < 0) { System.out.println("已达最小频道,无法下降!"); this.channel++; } } System.out.println(this.itemNO + "当前频道为:TV" + this.channel); } //重载:更换为指定频道 public void changeChannel(int newChannel) { //判断电视机是否开启,若未开启,则直接返回 if(!this.status) { System.out.println("电视机未开机!无法操作!"); return; } if(newChannel < 0 || newChannel > 100) { System.out.println("该频道不存在!"); } else { this.channel = newChannel; } System.out.println(this.itemNO + "当前频道为:TV" + this.channel); } //增减音量 public void adjustVolume(boolean adjust) { //判断电视机是否开启,若未开启,则直接返回 if(!this.status) { System.out.println("电视机未开机!无法操作!"); return; } if(adjust){ this.volume++; if(this.volume > 100) { System.out.println("已达音量最大值!无法提高音量!"); this.volume--; } } else { this.volume--; if(this.volume < 0) { System.out.println("已达音量最小值!无法降低音量!"); this.volume++; } } System.out.println(this.itemNO + "当前音量为:" + this.volume); } @Override public String toString() { return "Television{" + "itemNO='" + itemNO + '\'' + ", status=" + status + ", volume=" + volume + ", channel=" + channel + '}'; } }
列出测试数据和实验结果截图:
7、仿照超市购物的例子编写一个学生借书的程序。
提示:思考需要定义的类,例如:本程序需要用到学生、借书卡、书等对象,最后实现借书的过程,如果有指定的书,则输出“***借到了***书”,否则输出“****没有借到****书”。
还需要认真思考每个类中有哪些属性和方法,能够更好的完成这个程序。
本题相较于上面几题,稍微有些复杂。下面是思路:
1、学生类。学生类中需要有学生的基本信息,即学号、姓名。然后根据需求进行封装,提供相应的getter和setter接口;提供相应的构造方法;重写toString方法便于信息输出。
class Student{ private int stuID; private String name; public String getName() { return name; } public Student(int stuID, String name) { this.stuID = stuID; this.name = name; } @Override public String toString() { return "stuID=" + stuID + ", name='" + name; } }
2、图书类。图书类中也要提供图书的相关信息,包括图书号、图书名两个成员属性,相应的构造方法和toString方法。注意,不同的是,这里必须重写equals方法。这在判断学生要借的图书是否存在时,有重大用处。
class Book{ private int bookNo; private String name; public Book(int bookNo, String name) { this.bookNo = bookNo; this.name = name; } @Override public String toString() { return "[" + bookNo + ", " + name + ']'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Book book = (Book) o; return bookNo == book.bookNo && name.equals(book.name); } @Override public int hashCode() { return Objects.hash(bookNo, name); } }
3、图书馆类。图书馆类相当于是一个书架。用ArrayList集合进行图书的预存放。直接调用集合中的各种方法对图书进行操作,非常方便。当然,也可以自定义一维数组完成相同的功能。展示书架上的图书这一方法中用到了迭代器,因为这样可以自定义打印出来的格式。如果不使用迭代器,也可以直接System.out.print(libBook);因为ArrayList中重写过toString方法。
class Library{ //书架 List<Book> libBook; public Library() { this.libBook = new ArrayList<>(); } //设置书架上的图书 public void setBooks(Book book){ libBook.add(book); } //展示书架上的图书 public void showBooks() { Iterator it = libBook.listIterator(); while(it.hasNext()) { System.out.print(it.next() + " "); } System.out.println(); } }
4、借记卡类。借记卡的基本信息是卡号、学生、已借阅的图书与已借阅的图书数量这四个成员属性。其中,已借阅的图书也用ArrayList集合来存放。在构造方法中,只有学生的信息是需要外界传入的。卡号采取自动随机生成的方式赋值,采用了Random类来生成随机数。
在借书方法中,需要判断要借的书在书架上是否存在。采用的是ArrayList中的cntains方法进行判断。然而,在用contains将传入图书与已有图书比较的过程中,拿什么进行比较就很重要。java中ArrayList的contains方法,作用是用来判断元素是否在集合中,本质上使用的是比较对象的equals的方法来去比较。注意:若此时比较对象的equlas方法没有重写,则会进入Object的equlas方法比较地址,libBook中的任一元素与我们传入的book的地址肯定是不同的,因此一定会返回false。
而重写了equals方法,则能避免这些问题。若判断该书不存在,则抛出异常。
class DebitCard{ private int cardID; private Student stu; private List<Book> books; private int bookNum; //借记卡构造方法 public DebitCard(Student stu) { Random random = new Random(); this.cardID = random.nextInt(900)+100; this.stu = stu; this.books = new ArrayList<>(); } //借书 若书在图书馆中存在,则借记成功,若不存在,则抛出异常 public void borrowBooks(Book book,Library library) throws BookNotExistException{ if(library.libBook.contains(book)){ books.add(book); bookNum = books.size(); }else{ throw new BookNotExistException("该书不存在!无法借记!"); } } @Override public String toString() { return "DebitCard{" + "借记卡号:" + cardID + ", 已借阅图书:" + books + ", 已借阅图书数量:" + bookNum + '}'; } }
自定义的异常类:
public class BookNotExistException extends RuntimeException{ public BookNotExistException() { } public BookNotExistException(String message) { super(message); } }
5、设计好各个类之后,在main方法中进行测试即可。分为如下几步:预存放书架上的书、创建学生对象、注册借记卡、借书。
源代码:
import java.util.*; public class S3_7 { public static void main(String[] args) { //给图书馆书架上设置图书 Library library = new Library(); Book book1 = new Book(1111,"《Java程序设计》"); Book book2 = new Book(2222,"《C++程序设计》"); Book book3 = new Book(3333,"《Python程序设计》"); Book book4 = new Book(4444,"《Web程序设计》"); library.libBook.add(book1); library.libBook.add(book2); library.libBook.add(book3); library.libBook.add(book4); //展示书架上的图书 System.out.println("书架上现有图书:"); library.showBooks(); //创建学生对象 Scanner reader = new Scanner(System.in); System.out.println("请输入学生学号:"); int stuID = reader.nextInt(); System.out.println("请输入学生姓名:"); String stuName = reader.next(); Student stu = new Student(stuID,stuName); //申请借记卡 DebitCard debitCard = new DebitCard(stu); //展示借记卡信息 System.out.println(debitCard); //进行借书 System.out.println("请输入要借的图书编号:"); int bookNO = reader.nextInt(); System.out.println("请输入要借的图书名:"); String boName = reader.next(); try { debitCard.borrowBooks(new Book(bookNO,"《" + boName +"》"),library); }catch(BookNotExistException e){ System.out.println(stu.getName() + "没有借到" + boName); e.printStackTrace(); System.out.println(debitCard); System.exit(-1); } System.out.println(stu.getName() + "借到了" + boName); System.out.println(debitCard); } } class Student{ private int stuID; private String name; public String getName() { return name; } public Student(int stuID, String name) { this.stuID = stuID; this.name = name; } @Override public String toString() { return "stuID=" + stuID + ", name='" + name; } } class DebitCard{ private int cardID; private Student stu; private List<Book> books; private int bookNum; //借记卡构造方法 public DebitCard(Student stu) { Random random = new Random(); this.cardID = random.nextInt(900)+100; this.stu = stu; this.books = new ArrayList<>(); } //借书 若书在图书馆中存在,则借记成功,若不存在,则抛出异常 public void borrowBooks(Book book,Library library) throws BookNotExistException{ if(library.libBook.contains(book)){ books.add(book); bookNum = books.size(); }else{ throw new BookNotExistException("该书不存在!无法借记!"); } } @Override public String toString() { return "DebitCard{" + "借记卡号:" + cardID + ", 已借阅图书:" + books + ", 已借阅图书数量:" + bookNum + '}'; } } class Book{ private int bookNo; private String name; public Book(int bookNo, String name) { this.bookNo = bookNo; this.name = name; } @Override public String toString() { return "[" + bookNo + ", " + name + ']'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Book book = (Book) o; return bookNo == book.bookNo && name.equals(book.name); } /* @Override public int hashCode() { return Objects.hash(bookNo, name); }*/ } class Library{ //书架 List<Book> libBook; public Library() { this.libBook = new ArrayList<>(); } //设置书架上的图书 public void setBooks(Book book){ libBook.add(book); } //展示书架上的图书 public void showBooks() { Iterator it = libBook.listIterator(); while(it.hasNext()) { System.out.print(it.next() + " "); } System.out.println(); } }
public class BookNotExistException extends RuntimeException{ public BookNotExistException() { } public BookNotExistException(String message) { super(message); } }
列出测试数据和实验结果截图:
借记成功:
借记失败:
三、实验总结
1. 经过本次实验,我掌握了以下内容:
- 我理解了面向对象的相关基本概念,学会了如何声明类。通过构造方法实例化一个类,并通过对象名或类名进行调用。
- 我掌握了成员方法及构造方法的定义。成员方法可以实现类的各种功能,它有方法名、返回值和参数;构造方法用于对实例进行初始化。构造方法的名字必须与类名完全相同,且不能写返回值。
- 我掌握了方法重载的用法,方法重载就是方法之间的名称相同而参数列表不同,返回值没有影响。通过方法的重载,能够使代码更加灵活。
- 我理解了值传递和地址传递两种参数传递方式的区别。基本数据类型作为参数一般是值传递,也就是传递变量的内容;而引用数据类型作为参数则是地址传递。
- 我掌握了this关键字的用法。this关键字代表当前对象的引用。在同一个类中,this可以调用当前的成员变量与成员方法。语法是this.成员属性,this.成员方法(参数列表)。同时,this也可以用于在一个构造方法中调用另一构造方法,语法是this(),this(参数列表)。this调用构造方法只能出现在构造方法的第一句。
- 我掌握了static关键字的用法。static关键字修饰的成员属性或成员方法代表静态的成员属性或成员方法。类变量是脱离对象的,没有实例化的对象也可以使用类变量,它在类加载的时候会分配空间,通过类名进行调用。类中的所有对象共享静态成员(同一块堆内存空间,可以同时被多个栈内存所指向,不同的栈可以修改同一块堆内存的内容)。需要注意的是:静态方法中出现的成员变量只能是静态变量,出现的方法只能是静态方法,而不能是非静态的。static也可以修饰代码块,被其修饰的代码块为静态代码块。
- 引用数据类型与基本数据类型最大的不同在于引用数据类型需要内存的分配和使用。所以,关键字new的主要功能就是分配内存空间,只要使用引用数据类型就要使用关键字new来分配内存空间。
- 我掌握了访问控制符的使用以实现类的封装和信息隐蔽。在类中用private修饰成员属性,之后再提供相应的setter或getter方法来修改或获取值。
2、通过掌握以上内容,我能够初步进行类的设计,编写基本的面向对象的程序;
3. 遇到了一些语法上的问题,这是由于代码敲的不多,对语言不熟练。不会的地方通过翻书、查资料,基本能够解决问题。