面向对象编程(中级)
笔记目录:(https://www.cnblogs.com/wenjie2000/p/16378441.html)
lntelliJ IDEA
●IDEA介绍(内容仅需了解)
- IDEA全称Intelli IDEA
- 在业界被公认为最好的Java开发工具
- IDEA是JetBrains公司的产品,总部位于捷克的首都布拉格
- 除了支持Java开发,还支持HTML,CSS,PHP,MySQL,Python等
●Eclipse介绍
- Eclipse是一个开放源代码的、基于Java的可扩展开发平台。
- 最初是由IBM公司耗资3000万美金开发的下一代lD开发环境
- 2001年11月贡献给开源社区
- Eclipse是目前最优秀的Java开发IDE之一
IDEA的安装
官网:https://www.jetbrains.com/ IDEA下载后,就可以开始安装。
本人笔记中使用的 IDEA 2020.2
IDEA的基本介绍和使用
使用IDEA创建Java项目(project),看看IDEA是如何使用的,IDEA 是以项目的概念,来管理我们的java源码的
IDEA常用快捷键
- 删除当前行,自己配置ctrl + y
- 复制当前行,自己配置ctrl + d
- 补全代码alt +/
- 添加注释和取消注释ctrl +/【第一次是添加注释,第二次是取消注释】
- 快速格式化代码ctrl + alt +L
- 快速运行程序自己定义shift+f10
- 生成构造器等alt + insert , 选择constructor
- 查看一个类的层级关系ctrl+H[学习继承后,非常有用]
- 将光标放在一个方法上,输入ctrl +B(或ctrl+鼠标左键) ,可以选择定位到哪个类的方法【学继承后,/非常有用】
- 自动的分配变量名,通过在后面加 .var
●模板/自定义模板
file -> settings -> editor-> Live templates ->
查看有哪些模板快捷键/可以自己增加模板
public class TestTemplate {
//main就是一个模板的快捷键.
public static void main(String[] args) {
//sout模板快捷键
System.out.println("hello ,world");
//foni模板快捷键
for (int i = o; i <; i++) {
}
}
}
包
●看一个应用场景
现在有两个程序员共同开发一个java项目,程序员xiaoming希望定义一个类取名Dog ,程序员xiaoqiang也想定义一个类也叫 Dog。两个程序员为此还吵了起来,怎么办?
●包的三大作用
- 区分相同名字的类
- 当类很多时,可以很好的管理类[看Java API文档]
- 控制访问范围
●包基本语法
package com.hspedu;
●说明:
- package关键字,表示打包.
- com.hspedu:表示包名
●包的本质分析(原理)
包的本质实际上就是创建不同的文件夹来保存类文件。
//当出现多个包中的同class需要在同一个java文件中调用时
package com.use;
import com.xiaoqiang. Dog;
public class Test {
public static void main(String[] args){
Dog dog = new Dog();
System.out.println(dog) ;//com.xiaoqiang. Dog@1540e19d
com.xiaoming.Dog dog1 = new com.xiaoming.Dog();
System.out.println(dog1);//com.xiaoming. Dog@677327b6
}
}
●包的命名
命名规则:
只能包含数字、字母、下划线、小圆点.,但不能用数字开头,不能是关键字或保留字
命名规范
一般是小写字母+小圆点一般是
com.公司名.项目名.业务模块名
比如:com.hspedu.oa.model; com.hspedu.oa.controller;
举例:
com.sina.crm.user //用户模块
com.sina.crm.order //订单模块
com.sina.crm.utils //工具类
●常用的包
一个包下,包含很多的类,java中常用的包有:
java.lang.* //lang包是基本包,默认引入,不需要再引入.
java.util.* //util 包,系统提供的工具包,工具类,使用Scanner
java.net.* //网络包,网络开发
java.awt.* //是做java的界面开发,GUI
●如何引入包
com.hspedu.pkg : Import01.java
语法: import 包;
我们引入一个包的主要目的是要使用该包下的类
比如import java.util.Scanner;就只是引入一个类Scanner
import java.util.*;//表示将java.util包所有都引入
案例:使用系统提供 Arrays完成数组排序
●注意事项和使用细节
- package的作用是声明当前类所在的包,需要放在类的最上面,一个类中最多只有一句package
- import指令位置放在package的下面,在类定义前面,可以有多句且没有顺序要求。
访问修饰符
基本介绍
java提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围):
- 公开级别:用public修饰,对外公开
- 受保护级别:用protected修饰,对子类和同一个包中的类公开
- 默认级别:没有修饰符号,向同一个包的类公开.
- 私有级别:用private修饰,只有类本身可以访问,不对外公开.
4种访问修饰符的访问范围
访问级别 | 修饰符 | 当前类 | 同一包内 | 子类(不同包) | 其他包 |
---|---|---|---|---|---|
公开 | public |
Y | Y | Y | Y |
受保护 | protected |
Y | Y | Y | N |
默认 | 无 | Y | Y | N | N |
私有 | private |
Y | N | N | N |
●使用的注意事项
- 修饰符可以用来修饰类中的属性,成员方法以及类
- 只有默认的和public才能修饰类!,并且遵循上述访问权限的特点。
- 因为没有学习继承,因此关于在子类中的访问权限,我们讲完子类后,在回头讲解
- 成员方法的访问规则和属性完全一样.
⭐封装
面向对象编程三大特征
基本介绍
面向对象编程有三大特征:封装、继承和多态。
封装介绍
封装(encapsulation)就是把抽象出的数据[属性]和对数据的操作[方法]封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作[方法],才能对数据进行操作。
封装的理解和好处
- 隐藏实现细节
- 可以对数据进行验证,保证安全合理
封装的实现步骤
-
将属性进行私有化private【不能直接修改属性】
-
提供一个公共的set方法,用于对属性判断并赋值
public void setXxx(类型参数名){
//加入数据验证的业务逻辑(判断数据是否合理)
属性=参数名;
} -
提供一个公共的get方法,用于获取属性的值
public XX getXxx(){//权限判断
return xx;
}
●快速入门案例
看一个案例
那么在java中如何实现这种类似的控制呢?
请大家看一个小程序,不能随便查看人的年龄,工资等隐私,并对设置的年龄进行合理的验证。年龄合理就设置,否则给默认年龄,必须在1-120,年龄,工资不能直接查看,name的长度在2-6字符之间
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.setAge(126);
System.out.println(person.getAge());
}
}
class Person {
public String name;
private int age;
private double salary;
//alt+insert ,再选Getter and Setter,选择需要创建的
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age < 120 && age > 0) {
this.age = age;
} else {
System.out.println("年龄范围错误,默认设置18");
this.age = 18;
}
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
}
●将构造器和setXxx结合
public class Test {
public static void main(String[] args) {
......
}
}
class Person {
public String name;
private int age;
private double salary;
public Person() {
}
public Person(String name, int age, double salary) {
//this.name = name;
//this.age = age;
//this.salary = salary;
setName(name);
setAge(age);
setSalary(salary);
}
......
}
⭐继承
●为什么需要继承
我们编写了两个类,一个是Pupil类(小学生),一个是Graduate(研究生).问题:两个类的属性和方法有很多是相同的,怎么办?
●继承基本介绍和示意图
继承可以解决代码复用,让我们的编程更加靠近人类思维.当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends来声明继承父类即可。
画出继承的示意图
●继承的基本语法
class 子类 extends 父类{
}
- 子类就会自动拥有父类定义的属性和方法
- 父类又叫超类,基类。
- 子类又叫派生类。
class 父类 {
//共有属性
public String name;
public int age;
private double score;//成绩//共有的方法
public void setScore(double score) {
this.score = score;
}
}
class 子类 extends 父类 {
public void teting() {
System.out.println("小学生" +name + "正在考小学数学..");
}
}
●继承的深入讨论/细节问题
- 子类继承了所有的属性和方法,但是私有属性和方法不能在子类直接访问,要通过公共的方法去访问
- 子类必须调用父类的构造器,完成父类的初始化
- 当创建子类对象时,不管使用子类的哪个构造器,默认情况下(默认有super())总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的构造器中用super去指定使用父类的哪个构造器完成对父类的初始化工作,否则,编译不会通过
- 如果希望指定去调用父类的某个构造器,则显式的调用一下: super(参数列表)
- super在使用时,必须放在构造器第一行
- super()和this()都只能放在构造器第一行,因此这两个方法不能共存在一个构造器
- java所有类都是Object类的子类, Object是所有类的基类.
- 父类构造器的调用不限于直接父类!将一直往上追溯直到Object类(顶级父类)
- 子类最多只能继承一个父类(指直接继承),即java中是单继承机制。
思考:如何让A类继承B类和C类?【A继承B,B继承C】 - 不能滥用继承,子类和父类之间必须满足is-a的逻辑关系
●继承的本质分析(内存分析)(重要)
√案例
我们看一个案例来分析当子类继承父类,创建子类对象时,内存中到底发生了什么?
提示:当子类对象创建好后,建立查找的关系
public class ArrayTest {
public static void main(String[] args) {
Son son = new Son();//内存的布局
//?->这时请大家注意,要按照查找关系来返回信息
//(1)首先看子类是否有该属性
//(2)如果子类有这个属性,并且可以访问,则返回信息
//(3)如果子类没有这个属性,就看父类有没有这个属性(如果父类有该属性,并且可以访问,就返回信息..)
//(4〕如果父类没有就按照(3)的规则,继续找上级父类,直到0bject...
System.out.println(son.name);
System.out.println(son.hobby);
}
}
class GrandPa {
String name = "大头爷爷";
String hobby = "旅游";
}
class Father extends GrandPa {//父类
String name = "大头爸爸";
int age = 39;
}
class Son extends Father {//子类
String name = "大头儿子";
}
super关键字
基本介绍
super代表父类的引用,用于访问父类的属性、方法、构造器
基本语法
- 访问父类的属性,但不能访问父类的private属性
super.属性名; - 访问父类的方法,不能访问父类的private方法
super.方法名(参数列表); - 访问父类的构造器(这点前面用过):
super(参数列表);只能放在构造器的第一句,只能出现一句!
super给编程带来的便利/细节
- 调用父类的构造器的好处(分工明确,父类属性由父类初始化,子类的属性由子类初始化)
- 当子类中有和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过super。如果没有重名,使用super、this、直接访问是一样的效果!
- super的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用super去访问爷爷类的成员;如果多个基类(上级类)中都有同名的成员,使用super访问遵循就近原则。A->B->C。当然也需要遵守访问权限的相关规则
super和this的比较
区别点 | this | super |
---|---|---|
访问属性 | 访问本类中的属性,如果本类没有此属性则从父类中继续查找 | 从父类开始查找属性 |
调用方法 | 访问本类中的方法,如果本类没有此方法则从父类继续查找. | 从父类开始查找方法 |
调用构造器 | 调用本类构造器,必须放在构造器的首行 | 调用父类构造器,必须放在子类构造器的首行 |
特殊 | 表示当前对象 | 子类中访问父类对象 |
方法重写/覆盖(override)
基本介绍
简单的说:方法覆盖(重写)就是子类有一个方法,和父类的某个方法的名称、返回类型、参数一样,那么我们就说子类的这个方法覆盖了父类的方法
注意事项和使用细节
方法重写也叫方法覆盖,需要满足下面的条件
- 子类的方法的形参列表,方法名称,要和父类方法的参数,方法名称完全一样。【演示】
- 子类方法的返回类型和父类方法返回类型一样,或者是父类返回类型的子类
比如父类返回类型是Object,子类方法返回类型是String
public Object getInfo(){ 和 public String getInfo(){ 构成方法重写 - 子类方法不能缩小父类方法的访问权限 public > protected >默认>private
例:父类中方法为public void sayok(){ ,子类中方法为void sayok(){会报错
重写和重载比较
名称 | 发生范围 | 方法 | 形参列表 | 返回类型 | 修饰符 |
---|---|---|---|---|---|
重载(overload) | 本类 | 必须一样 | 类型,个数或者顺序至少有一个不同 | 无要求 | 无要求 |
重写(override) | 父子类 | 必须一样 | 相同 | 子类重写的方法,返回的类型和父类返回的类型一致,或者是其子类 | 子类方法不能缩小父类方法的访问范围 |
⭐多态
先看一个问题
问题描述:请编写一个程序,Master类中有一个 feed(喂食)方法,可以完成主人给动物喂食物的信息。
传统的方法带来的问题是什么?如何解决?
问题是:每次都要创建新的方法存放动物种类。代码的复用性不高,而且不利于代码维护
解决方案:引出我们要讲解的多态
多[多种]态[状态]基本介绍
方法或对象具有多种形态。是面向对象的第三大特征,多态是建立在封装和继承基础之上的。
多态的具体体现
-
方法的多态
重写和重载就体现多态
-
对象的多态(核心,困难,重点)老韩重要的几句话(记住):
(1)一个对象的编译类型和运行类型可以不一致
(2)编译类型在定义对象时,就确定了,不能改变
(3)运行类型是可以变化的.
(4)编译类型看定义时=号的左边,运行类型看=号的右边
案例:
Animal animal = new Dog();【animal编译类型是Animal,运行类型Dog】
animal = new Cat();【animal的运行类型变成了Cat,编译类型仍然是Animal】
一个父类的对象引用可以指向其子类对象
多态快速入门案例
使用多态的机制来解决主人喂食物的问题,走代码。
传入的实参是 cat和fish,对应形参可以为animal和food
多态注意事项和细节讨论
多态的前提是:两个对象(类)存在线承关系
多态的向上转型
- 本质:父类的引用指向了子类的对象
- 语法:父类类型引用名= new子类类型0;
- 特点:编译类型看左边,运行类型看右边。
可以调用父类中的所有成员(需遵守访问权限),不能调用子类中特有成员;
最终运行效果看子类的具体实现!
向上转型调用方法的规则如下:
-
可以调用父类中的所有成员(需遵守访问权限)
-
但是不能调用子类的特有的成员
-
因为在编译阶段,能调用哪些成员,是由编译类型来决定的
-
最终运行效果看子类(运行类型)的具体实现,即调用方法时,按照从子类(运行类型)开始查找方法
然后调用,规则我前面我们讲的方法调用规则一致。
多态注意事项和细节讨论
多态的向下转型
-
语法:子类类型 引用名 = (子类类型) 父类引用;【例:Cat cat=(Cat) animal】
-
只能强转父类的引用,不能强转父类的对象
-
要求父类的引用必须指向的是当前目标类型的对象
-
可以调用子类类型中所有的成员
多态注意事项和细节
-
属性没有重写之说!属性的值看编译类型(运行调用属性时认准编译类型,在方法中调用属性则是调用该方法所在类中的属性)
-
instanceof比较操作符,用于判断对象的运行类型是否为XX类型或XX类型的子类型
packapublic class Poly2 { public static void main(String[] args) { Base base=new Sub(); base.Aa();//111111 System.out.println(base.count);//10 Sub sub=(Sub) base; System.out.println(sub.count);//20 System.out.println(base instanceof Base);//true System.out.println(base instanceof Sub);//true Object obj = new Object(); System.out.println(obj instanceof Base); //false } } class Base {//父类 int count = 10;//属性 public void Aa(){ System.out.println("00000"); } } class Sub extends Base {//子类 int count = 20;//属性 public void Aa(){ System.out.println("111111"); } }
java的动态绑定机制(非常非常重要.)
- 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定
- 当调用对象属性时,没有动态绑定机制,哪里声明,那里使用
多态的应用
- 多态数组(Person[] persons = new Person[5];类似于创建数组;其中有persons[0]到person[4]五个对象)
数组的定义类型为父类类型,里面保存的实际元素类型为子类类型 - 多态参数
方法定义的形参类型为父类类型,实参类型允许为子类类型
Object类详解
equals方法
“==” 和 “equals” 的对比[面试题]
- ==:既可以判断基本类型,又可以判断引用类型
- ==:如果判断基本类型,判断的是值是否相等。示例: int i=10; double d=10.0;
- ==:如果判断引用类型,判断的是地址是否相等,即判定是不是同一个对象。
- equals:是Object类中的方法,只能判断引用类型(如何看Jdk源码:在对应方法位置ctrl+鼠标左键)
- 默认判断的是地址是否相等,子类中往往重写该方法,用于判断内容是否相等。比如Integer,String【看看String和 Integer的equals源代码】
hashCode方法
- 提高具有哈希结构的容器的效率!、
- 两个引用,如果指向的是同一个对象,则哈希值肯定是一样的!
- 两个引用,如果指向的是不同对象,则哈希值是不一样的
- 哈希值主要根据地址号来的!,不能完全将哈希值等价于地址。
- 案例演示[HashCode_,java]: obj.hashCode()【测试: A obj1 = new A(); A obj2 = new AO);A
obj3 = obj1】 - 后面在集合,中hashCode 如果需要的话,也会重写
toString方法
基本介绍
- 默认返回:全类名+@+哈希值的十六进制,【查看Object的toString方法】子类往往重写toString方法,用于返回对象的属性信息
- 重写toString方法,打印对象或拼接对象时,都会自动调用该对象的toString形式.案例演示: Monster [name, job, sal]案例:ToString .java
- 当直接输出一个对象时,toString方法会被默认的调用,比如System.out.println(monster).
finalize方法
- 当对象被回收时,系统自动调用该对象的finalize方法。子类可以重写该方法,做一些释放资源的操作【演示】
- 什么时候被回收:当某个对象没有任何引用时,则jvm就认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用finalize方法。
- 垃圾回收机制的调用,是由系统来决定,也可以通过System.gc()主动触发垃圾回收机制,测试:Car [name]
提示:我们在实际开发中,几乎不会运用finalize,所以更多就是为了应付面试.
断点调试(debug)
一个实际需求
- 在开发中,新手程序员在查找错误时,这时老程序员就会温馨提示,可以用断点调试,一步一步的看源码执行的过程,从而发现错误所在。
- 重要提示:在断点调试过程中,是运行状态,是以对象的运行类型来执行的.
断点调试介绍
- 断点调试是指在程序的某一行设置一个断点,调试时,程序运行到这一行就会停住,然后你可以一步一步往下调试,调试过程中可以看各个变量当前的值,出错的话,调试到出错的代码行即显示错误,停下。进行分析从而找到这个Bug
- 断点调试是程序员必须掌握的技能。
- 断点调试也能帮助我们查看java底层源代码的执行过程,提高程序员的Java水平。
断点调试的快捷键:
F7(跳入) F8(跳过) shift+F8(跳出) F9(resume,执行到下一个断点)
F7:跳入方法内
F8:逐行执行代码.
shift+F8:跳出方法
小技巧:将光标放在某个变量上,可以看到最新的数据。
Alt+Shift+F7强制跳入(可以用来看数据在源码中的情况)
项目-零钱通
项目需求说明
使用Java 开发零钱通项目,可以完成收益入账,消费,查看明细,退出系统等功能.
项目界面
项目的界面
化繁为简.
-
先完成显示菜单,并可以选择
import java.util.Scanner; public class SmallChangeSys { public static void main(String[] args) { boolean loop=true; Scanner scanner = new Scanner(System.in); String key=""; do { System.out.println("==========零钱通菜单==========="); System.out.println("\t\t1 零钱通明细"); System.out.println("\t\t2 收益入账"); System.out.println("\t\t3 消费"); System.out.println("\t\t4 退 出"); System.out.println("请选择(1-4):"); key=scanner.next(); switch (key){ case "1": System.out.println("1 零钱通"); break; case "2": System.out.println("2 收益入账"); break; case "3": System.out.println("3 消费"); break; case "4": System.out.println("4 退 出"); loop=false; break; default: System.out.println("选择有误"); } }while (loop); System.out.println("---退出了零钱通---"); } }
-
完成零钱通明细
-
完成收益入账
-
完成消费
-
完成退出功能
-
加入简单的逻辑判断
import java.text.SimpleDateFormat; import java.util.Date; import java.util.Scanner; public class SmallChangeSys { public static void main(String[] args) { boolean loop = true; Scanner scanner = new Scanner(System.in); String key = ""; String details = "----------------零钱通明细------------------"; //3.完成收益入账完成功能驱动程序员增加新的变化和代码/老韩思路,定义新的变量 double money = 0; double balance = 0; Date date = null; // date 是 java.util.Date类型,表示日期 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");//可以用于日期格式化的 String note = ""; do { System.out.println("\n==========零钱通菜单==========="); System.out.println("\t\t1 零钱通明细"); System.out.println("\t\t2 收益入账"); System.out.println("\t\t3 消费"); System.out.println("\t\t4 退 出"); System.out.println("请选择(1-4):"); key = scanner.next(); switch (key) { case "1": System.out.println(details); break; case "2": System.out.print("收益入账金额:"); money = scanner.nextDouble();// money的值范围应该校验-》一会在完善 if (money <= 0) { System.out.printf("收益入账金额需要大于0"); break; } balance += money; //拼接收益入账信息到details date = new Date();//获取当前日期 details += "\n收益入账\t+" + money + "\t" + sdf.format(date) + "\t" + balance; break; case "3": System.out.println("3 消费"); System.out.println("消费金额:"); money = scanner.nextDouble(); if (money <= 0 || money > balance) { System.out.printf("消费金额应该在0-" + balance + "范围内"); } //money 的值范围应该校验-》一会在完善 System.out.print("消费说明:"); note = scanner.next(); balance -= money; //拼接消费信息到details date = new Date();//获取当前日期 details += "\n" + note + "\t-" + money + "\t" + sdf.format(date) + "\t" + balance; break; case "4": String choice = ""; while (true) { System.out.printf("确定退出?y/n"); choice = scanner.next(); if ("y".equals(choice) || "n".equals(choice)) { break; } } if (choice.equals("y")) { loop = false; } break; default: System.out.println("选择有误"); } } while (loop); System.out.println("---退出了零钱通---"); } }
-
再将程序改成OOP版本,自行体会OOP编程带来的好处(将各个功能封装到不同方法)
import java.text.SimpleDateFormat; import java.util.Date; import java.util.Scanner; public class SmallChangeSysOOP { boolean loop = true; Scanner scanner = new Scanner(System.in); String key = ""; String details = "----------------零钱通明细------------------"; //3.完成收益入账完成功能驱动程序员增加新的变化和代码/老韩思路,定义新的变量 double money = 0; double balance = 0; Date date = null; // date 是 java.util.Date类型,表示日期 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");//可以用于日期格式化的 String note = ""; //先完成显示菜单,并可以选择 public void mainMenu() { do { System.out.println("\n==========零钱通菜单==========="); System.out.println("\t\t1 零钱通明细"); System.out.println("\t\t2 收益入账"); System.out.println("\t\t3 消费"); System.out.println("\t\t4 退 出"); System.out.println("请选择(1-4):"); key = scanner.next(); switch (key) { case "1": this.detail(); break; case "2": this.income(); break; case "3": this.pay(); break; case "4": this.exit(); break; default: System.out.println("选择有误"); } } while (loop); } //完成零钱通明细 public void detail() { System.out.println(details); } public void income(){ System.out.print("收益入账金额:"); money = scanner.nextDouble();// money的值范围应该校验-》一会在完善 if (money <= 0) { System.out.printf("收益入账金额需要大于0"); return; } balance += money; //拼接收益入账信息到details date = new Date();//获取当前日期 details += "\n收益入账\t+" + money + "\t" + sdf.format(date) + "\t" + balance; } public void pay(){ System.out.println("3 消费"); System.out.println("消费金额:"); money = scanner.nextDouble(); if (money <= 0 || money > balance) { System.out.printf("消费金额应该在0-" + balance + "范围内"); } //money 的值范围应该校验-》一会在完善 System.out.print("消费说明:"); note = scanner.next(); balance -= money; //拼接消费信息到details date = new Date();//获取当前日期 details += "\n" + note + "\t-" + money + "\t" + sdf.format(date) + "\t" + balance; } public void exit(){ String choice = ""; while (true) { System.out.printf("确定退出?y/n"); choice = scanner.next(); if ("y".equals(choice) || "n".equals(choice)) { break; } } if (choice.equals("y")) { loop = false; } } } class SmallChangeSysApp{ public static void main(String[] args) { SmallChangeSysOOP smallChangeSysOOP = new SmallChangeSysOOP(); smallChangeSysOOP.mainMenu(); } }