13 第十三章 方法覆盖和多态(Polymorphism)★★★★★
13.1 章节目标与知识框架
13.1.1 章节目标
理解在什么情况下我们需要进行方法覆盖?掌握在满足什么条件的时候构成方法覆盖?什么是多态,代码怎么写?向上转型和向下转型都是什么?多态在开发中有什么作用?
13.1.2 知识框架
13.2 方法覆盖Override
13.2.1 什么时候需要方法覆盖(理解)
学习方法覆盖之前,我们先来回顾一下方法重载(overload),什么情况下考虑使用方法重载呢?在同一个类当中,如果功能相似,尽可能将方法名定义的相同,这样方便调用的同时代码也会美观。那么,代码满足什么条件的时候能够构成方法重载呢?只要在同一个类当中,方法名相同,参数列表不同(类型、个数、顺序),即构成方法重载。
带着同样的疑问去学习方法覆盖,什么是方法覆盖?什么情况下考虑方法覆盖?代码怎么写的时候就构成了方法覆盖呢?接下来看一段代码:
运行结果如下图所示:
“中国人”调用speakHi()方法希望输出的结果是“你好,我叫张三,很高兴见到你!”,“美国人”调用speakHi()方法更希望输出的结果是“Hi,Mynameisjackson,Nicetomeetyou!”,可见ChinaPeople和AmericaPeople从父类中继承过来的speakHi()方法已经不够子类使用了,那这个时候应该怎么办呢?当然,此时就需要使用方法覆盖机制了。
13.2.2 怎么进行方法覆盖(掌握)
针对上一小节的问题,我们应该怎么进行方法覆盖呢,请看以下代码:
运行结果如下图所示:
以上程序中ChinaPeople和AmericaPeople将从People类中继承过来的speakHi()方法进行了覆盖,我们也看到了当speakHi()方法发生覆盖之后,子类对象会调用覆盖之后的方法,不会再去调用之前从父类中继承过来的方法。
那么,到底在什么情况下我们会考虑使用方法覆盖呢?通过以上内容的学习,我们了解到只有当从父类中继承过来的方法无法满足当前子类业务需求的时候,需要将父类中继承过来的方法进行覆盖。换句话说,父类中继承过来的方法已经不够用了,子类有必要将这个方法重新再写一遍,所以方法覆盖又被称为方法重写。当该方法被重写之后,子类对象一定会调用重写之后的方法。
13.2.3 方法覆盖的条件及注意事项(掌握)
那么,当程序具备哪些条件的时候,就能构成方法覆盖呢?
①方法覆盖发生在具有继承关系的父子类之间,这是首要条件;
②覆盖之后的方法与原方法具有相同的返回值类型、相同的方法名、相同的形式参数列表;
另外,在使用方法覆盖的时候,需要有哪些注意事项呢?
①由于覆盖之后的方法与原方法一模一样,建议在开发的时候采用复制粘贴的方式,不建议手写,因为手写的时候非常容易出错,比如在Object类当中有toString()方法,该方法中的S是大写的,在手写的时候很容易写成小写tostring(),这个时候你会认为toString()方法已经被覆盖了,但由于方法名不一致,导致最终没有覆盖,这样就尴尬了;
②私有的方法不能被继承,所以不能被覆盖;
③构造方法不能被继承,所以也不能被覆盖;
④覆盖之后的方法不能比原方法拥有更低的访问权限,可以更高(学习了访问控制权限修饰符之后你就明白了);
⑤覆盖之后的方法不能比原方法抛出更多的异常,可以相同或更少(学习了异常之后就明白了);
⑥方法覆盖只是和方法有关,和属性无关;
⑦静态方法不存在覆盖(不是静态方法不能覆盖,是静态方法覆盖意义不大,学习了多态机制之后就明白了);
以上的注意事项还需要大家记忆,多下点功夫吧。接下来我们再来看一段代码,对方法覆盖加深一下印象,业务需求是这样的:定义一个动物类,所有动物都有移动的行为,其中猫类型的对象在移动的时候输出“猫在走猫步!”,鸟儿类型的对象在移动的时候输出“鸟儿在飞翔!”,但是猫类型的对象具有一个特殊的行为,抓老鼠,这个行为不是所有动物对象都有的,是猫类型对象特有的:
运行结果如下图所示:
对方法覆盖总结一下,当父类中继承过来的方法无法满足当前子类业务需求的时候,子类有必要将父类中继承过来的方法进行覆盖/重写。方法覆盖发生在具有继承关系的父子类之间,方法覆盖的时候要求相同的返回值类型、相同的方法名、相同的形式参数列表。方法覆盖之后子类对象在调用的时候一定会执行覆盖之后的方法。
13.3 多态
13.3.1 多态基础语法(掌握)
多态(Polymorphism)属于面向对象三大特征之一,它的前提是封装形成独立体,独立体之间存在继承关系,从而产生多态机制。多态是同一个行为具有多个不同表现形式或形态的能力。现实中,比如我们按下F1键这个动作:
如果当前在Flash界面下弹出的就是AS3的帮助文档;
如果当前在Word下弹出的就是Word帮助;
如果当前在Windows下弹出的就是Windows帮助和支持。
多态就是“同一个行为”发生在“不同的对象上”会产生不同的效果。那么在java中多态是如何体现的呢?
在java中允许这样的两种语法出现,一种是向上转型(Upcasting),一种是向下转型(Downcasting),向上转型是指子类型转换为父类型,又被称为自动类型转换,向下转型是指父类型转换为子类型,又被称为强制类型转换。请看下图:
在java语言中有这样的一个规定,无论是向上转型还是向下转型,两种类型之间必须要有继承关系,没有继承关系情况下进行向上转型或向下转型的时候编译器都会报错,这一点要死记硬背哦!
接下来我们来看一段代码:
运行结果如下图所示:
其实在java中还允许这样写代码,请看:
运行结果如下图所示:
以上程序演示的就是多态,多态就是“同一个行为(move)”作用在“不同的对象上”会有不同的表现结果。java中之所以有多态机制,是因为java允许一个父类型的引用指向一个子类型的对象。也就是说允许这种写法:Animal a2 = new Bird(),因为Bird is a Animal是能够说通的。其中Animal a1 = new Cat()或者Animal a2 = new Bird()都是父类型引用指向了子类型对象,都属于向上转型(Upcasting),或者叫做自动类型转换。
我来解释一下这段代码片段【Animal a1 = new Cat(); a1.move();】:java程序包括编译和运行两个阶段,分析java程序一定要先分析编译阶段,然后再分析运行阶段,在编译阶段编译器只知道a1变量的数据类型是Animal,那么此时编译器会去Animal.class字节码中查找move()方法,发现Animal.class字节码中存在move()方法,然后将该move()方法绑定到a1引用上,编译通过了,这个过程我们可以理解为“静态绑定”阶段完成了。紧接着程序开始运行,进入运行阶段,在运行的时候实际上在堆内存中new的对象是Cat类型,也就是说真正在move移动的时候,是Cat猫对象在移动,所以运行的时候就会自动执行Cat类当中的move()方法,这个过程可以称为“动态绑定”。但无论是什么时候,必须先“静态绑定”成功之后才能进入“动态绑定”阶段。
来看以下的一段代码以及编译结果:
编译结果:
有人认为Cat猫是可以抓老鼠的呀,为什么会编译报错呢?那是因为“Animal a = new Cat();”在编译的时候,编译器只知道a变量的数据类型是Animal,也就是说它只会去Animal.class字节码中查找catchMouse()方法,结果没找到,自然“静态绑定”就失败了,编译没有通过。就像以上描述的错误信息一样:在类型为Animal的变量a中找不到方法catchMouse()。
那么,假如说我就是想让这只猫去抓老鼠,以上代码应该如何修改呢?请看以下代码:
运行结果如下图所示:
我们可以看到直接使用a引用是无法调用catchMouse()方法的,因为这个方法属于子类Cat中特有的行为,不是所有Animal动物都可以抓老鼠的,要想让它去抓老鼠,就必须做向下转型(Downcasting),也就是使用强制类型转换将Animal类型的a引用转换成Cat类型的引用c(Cat c = (Cat)a;),使用Cat类型的c引用调用catchMouse()方法。
通过这个案例,可以得出:只有在访问子类型中特有数据的时候,需要先进行向下转型。其实向下转型就是用在这种情形之下。那么向下转型会存在什么风险吗?请看以下代码:
以上代码可以编译通过吗?答案是可以的,为什么呢?那是因为编译器只知道a变量是Animal类型,Animal类和Cat类之间存在继承关系,所以可以进行向下转型(前面提到过,只要两种类型之间存在继承关系,就可以进行向上或向下转型),语法上没有错误,所以编译通过了。但是运行的时候会出问题吗,因为毕竟a引用指向的真实对象是一只小鸟。来看运行结果:
以上的异常是很常见的ClassCastException,翻译为类型转换异常,这种异常通常出现在向下转型的操作过程当中,当类型不兼容的情况下进行转型出现的异常,之所以出现此异常是因为在程序运行阶段a引用指向的对象是一只小鸟,然后我们要将一只小鸟转换成一只猫,这显然是不合理的,因为小鸟和猫之间是没有继承关系的。为了避免这种异常的发生,建议在进行向下转型之前进行运行期类型判断,这就需要我们学习一个运算符了,它就是instanceof。
instanceof运算符的语法格式是这样的:
(引用 instanceof类型)
instanceof运算符的运算结果是布尔类型,可能是true,也可能是false,假设(c instanceof Cat)结果是true则表示在运行阶段c引用指向的对象是Cat类型,如果结果是false则表示在运行阶段c引用指向的对象不是Cat类型。有了instanceof运算符,向下转型就可以这样写了:
以上程序运行之后不再发生异常,并且什么也没有输出,那是因为if语句的条件并没有成立,因为在运行阶段a引用指向的对象不是Cat类型,所以(a instanceof Cat)是false,自然就不会进行向下转型了,也不会出现ClassCastException异常了。在实际开发中,java中有这样一条默认的规范需要大家记住:在进行任何向下转型的操作之前,要使用instanceof进行判断,这是一个很好的编程习惯。就像下面的代码:
运行结果如下图所示:
到这里大家理解什么是多态了吗?其实多态存在的三个必要条件分别是:
①继承
②方法覆盖
③父类型引用指向子类型对象
多态显然是离不开方法覆盖机制的,多态就是因为编译阶段绑定父类当中的方法,程序运行阶段自动调用子类对象上的方法,如果子类对象上的方法没有进行重写,这个时候创建子类对象就没有意义了,自然多态也就没有意义了,只有子类将方法重写之后调用到子类对象上的方法产生不同效果时,多态就形成了。实际上方法覆盖机制和多态机制是捆绑的,谁也离不开谁,多态离不开方法覆盖,方法覆盖离开了多态也就没有意义了。
接下里就来看看之前没有解决的问题:方法覆盖主要是说实例方法,静态方法为什么不谈方法覆盖?
运行结果如下图所示:
我们发现貌似也发生了覆盖,在程序运行的时候确实也调用了“子类MathSubClass”的sum方法,但这种“覆盖”有意义吗?其实上面的课程我们已经说过了,方法覆盖和多态机制联合起来才有意义,我们来看看这种“覆盖”是否能够达到“多态”的效果,请看代码:
运行结果如下图所示:
通过以上的代码,我们发现虽然创建了子类型对象“new MathSubClass()”,但是程序在运行的时候仍然调用的是Math类当中的sum方法,甚至m = null的时候再去调用m.sum()也没有出现空指针异常,这说明静态方法的执行压根和对象无关,既然和对象无关那就表示和多态无关,既然和多态无关,也就是说静态方法的“覆盖”是没有意义的,所以通常我们不谈静态方法的覆盖。
13.3.2 多态在开发中的作用(理解)
以上学习了多态的基础语法,多态在实际开发中有什么作用呢?我们先来了解一个业务背景:请设计一个系统,描述主人喂养宠物的场景,首先在这个场景当中应该有“宠物对象”,宠物对象应该有一个吃的行为,另外还需要一个“主人对象”,主人对象应该有一个喂的行为,请看代码:
运行结果如下图所示:
以上程序编译和运行都很正常,输出结果也是对的,那么存在什么问题吗?假设后期用户提出了新的需求,软件可能面临着功能扩展,这个扩展会很方便吗?假设现在主人家里又来了一个宠物猫,那该怎么办呢?请看代码:
在以上代码的基础之上,新增了一个Cat类,来表示宠物猫,这个对于程序来说是可以接受的:
另外,除了增加一个Cat类之外,我们还需要“修改”Master主人类的源代码,这件事儿是我们程序员无法容忍的,因为修改之前写好的源代码就面临着重新编译、重新全方位的测试,这是一个巨大的工作,维护成本很高,也很麻烦:
运行结果如下图所示:
在软件开发过程中,有这样的一个开发原则:开闭原则。开闭原则(OCP)是面向对象设计中“可复用设计”的基石,是面向对象设计中最重要的原则之一,其它很多的设计原则都是实现开闭原则的一种手段。1988年,勃兰特·梅耶(Bertrand Meyer)在他的著作《面向对象软件构造(Object Oriented Software Construction)》中提出了开闭原则,它的原文是这样:“Software entities should be open for extension, but closed for modification”。翻译过来就是:“软件实体应当对扩展开放,对修改关闭”。这句话说得略微有点专业,我们把它讲得更通俗一点,也就是:软件系统中包含的各种组件,例如模块(Modules)、类(Classes)以及功能(Functions)等等,应该在不修改现有代码的基础上,引入新功能。开闭原则中“开”,是指对于组件功能的扩展是开放的,是允许对其进行功能扩展的;开闭原则中“闭”,是指对于原有代码的修改是封闭的,即修改原有的代码对外部的使用是透明的。
以上程序在扩展的过程当中就违背了OCP原则,因为在扩展的过程当中修改了已经写好的Master类,怎样可以解决这个问题呢?多态可以解决,请看代码:
运行结果如下图所示:
在以上程序中,Master类中的方法feed(Petpet)的参数类型定义为更加抽象的Pet类型,而不是具体Dog宠物,或者Cat宠物,显然Master类和具体的Dog、Cat类解耦合了,依赖性弱了,这就是我们通常所说的面向抽象编程,尽量不要面向具体编程,面向抽象编程会让你的代码耦合度降低,扩展能力增强,从而符合OCP的开发原则。假如说这会再来一个新的宠物猪呢,我们只需要这样做,新增加一个“宠物猪类”,然后宠物猪类Pig继承宠物类Pet,并重写eat()方法,然后修改一下测试类就行了,整个过程我们是不需要修改Master类的,只是额外增加了一个新的类:
运行结果如下图所示:
以上程序中到底哪里使用了多态机制呢?请看下图:
通过以上内容的学习,我们可以看到多态在开发中联合方法覆盖一起使用,可以降低程序的耦合度,提高程序的扩展力。在开发中尽可能面向抽象编程,不要面向具体编程,好比电脑主板和内存条的关系一样,主板和内存条件之间有一个抽象的符合某个规范的插槽,不同品牌的内存条都可以插到主板上使用,2个G的内存条和4个G的内存条都可以插上,但最终的表现结果是不同的,2个G的内存条处理速度慢一些,4个G的快一些,这就是多态,所谓多态就是同一个行为作用到不同的对象上,最终的表现结果是不同的,主要的要求就是对象是可以进行灵活切换的,灵活切换的前提就是解耦合,解耦合依赖多态机制。
13.4 章节小结
通过本章节内容的学习,大家需要理解什么时候考虑使用方法覆盖,理解满足什么条件时能够构成方法覆盖,以及掌握怎么进行方法覆盖。另外还需要掌握多态相关的基础语法,理解多态在开发中起到了解耦合的作用,提倡面向抽象编程,不要面向具体编程。理解OCP开发原则等。
13.5 难点疑惑
本章节的难点就是对多态机制的理解,多态的代码表现是父类型引用指向子类型对象,对于多态的理解一定要分为编译阶段和运行阶段来进行分析,编译阶段只是看父类型中是否存在要调用的方法,如果父类中不存在,则编译器会报错,编译阶段和具体new的对象无关。但是在运行阶段就要看底层具体new的是哪个类型的子对象了,new的这个子类型对象可以看做“真实对象”,自然在运行阶段就会调用真实对象的相关方法。例如代码:Animal a = new Cat(); a.move();,在编译阶段编译器只能检测到a的类型是Animal,所以一定会去Animal类中找move()方法,如果Animal中没有move()方法,则编译器会报错,即使Cat中有move()方法,也会报错,因为编译器只知道a的类型是Animal类,只有在运行的时候,实际创建的真实对象是Cat,那么在运行的时候就会自动调用Cat对象的move()方法。这样就可以达到多种形态,也就是说编译阶段一种形态,运行阶段的时候是另一种形态。这也就是多态的含义。
13.6 章节习题
第一题:实现愤怒的小鸟
我们有很多种小鸟,每种小鸟都有飞的行为,还有一个弹弓,弹弓有一个弹射的行为,弹射时把小鸟弹出去,之后小鸟使用自己飞行的行为飞向小猪(不要求编写小猪的代码)。不同种类的小鸟有不同飞行的方式:
红火:红色小鸟,飞行方式:正常
蓝冰:蓝色小鸟,飞行方式:分成3个
黄风:黄色小鸟,飞行方式:加速。
第二题:计算不同类型的员工薪资
定义员工类Employee,员工包含姓名name、出生月份birthMonth两个属性,员工有获取指定月份工资的方法(getSalary(int month)),如果该月员工生日,公司补助250元。
定义有固定工资的员工类SalariedEmployee,有月薪monthlySalary属性。
定义小时工类HourlyEmployee,包含工作小时数hours和每小时的工资hourlySalary属性,如果每月工作超过160小时,超过的部分按1.5倍工资发放。
定义销售人员类SalesEmployee,包含月销售额sales和提成比例comm属性。
第三题:某汽车租赁公司有多种汽车可以出租,计算汽车租金
Vehicle是所有车的父类,属性:品牌、车牌号,有返回总租金的方法:public double getSumRent(int days){}
小轿车类Car是Vehicle的子类,属性:车型(两厢、三厢、越野),两厢每天300,三厢每天350,越野每天500。
多座汽车类Bus是Vehicle的子类,属性:座位数,座位数<=16的每天400,座位数>16的每天600。
编写测试类,根据用户选择不同的汽车,计算总租金。
13.7 习题答案
第一题答案:
执行结果如下图所示:
第二题答案:
执行结果如下图所示:
第三题答案:
执行结果如下图所示:
13.8 day18课堂笔记
上接day17
...
2、方法覆盖
2.1、什么时候考虑使用方法覆盖?
父类中的方法无法满足子类的业务需求,子类有必要对继承过来的方法进行覆盖。
2.2、什么条件满足的时候构成方法覆盖?
第一:有继承关系的两个类
第二:具有相同方法名、返回值类型、形式参数列表
第三:访问权限不能更低。
第四:抛出异常不能更多。
2.3、关于Object类中toString()方法的覆盖?
toString()方法存在的作用就是:将java对象转换成字符串形式。
大多数的java类toString()方法都是需要覆盖的。因为Object类中提供的toString()方法输出的是一个java对象的内存地址。
至于toString()方法具体怎么进行覆盖?
格式可以自己定义,或者听需求的。(听项目要求的。)
2.4、方法重载和方法覆盖有什么区别?
方法重载发生在同一个类当中。
方法覆盖是发生在具有继承关系的父子类之间。
方法重载是一个类中,方法名相同,参数列表不同。
方法覆盖是具有继承关系的父子类,并且重写之后的方法必须和之前的方法一致:方法名一致、参数列表一致、返回值类型一致。
3、多态的基础语法
3.1、向上转型和向下转型的概念。
向上转型:子--->父 (upcasting)
又被称为自动类型转换:Animal a = new Cat();
向下转型:父--->子 (downcasting)
又被称为强制类型转换:Cat c = (Cat)a; 需要添加强制类型转换符。
什么时候需要向下转型?
需要调用或者执行子类对象中特有的方法,必须进行向下转型,才可以调用。
向下转型有风险吗?
容易出现ClassCastException(类型转换异常)
怎么避免这个风险?
instanceof运算符,可以在程序运行阶段动态的判断某个引用指向的对象是否为某一种类型。
养成好习惯,向下转型之前一定要使用instanceof运算符进行判断。
不管是向上转型还是向下转型,首先他们之间必须有继承关系,这样编译器就不会报错。
3.2、什么是多态。
多种形态,多种状态,编译和运行有两个不同的状态。
编译期叫做静态绑定。
运行期叫做动态绑定。
Animal a = new Cat();
// 编译的时候编译器发现a的类型是Animal,所以编译器会去Animal类中找move()方法
// 找到了,绑定,编译通过。但是运行的时候和底层堆内存当中的实际对象有关
// 真正执行的时候会自动调用“堆内存中真实对象”的相关方法。
a.move();
多态的典型代码:父类型的引用指向子类型的对象。(java中允许这样写代码!!!)
3.3、什么时候必须进行向下转型?
调用子类对象上特有的方法时。
1、多态在开发中有什么作用?
非常重要:五颗星。。。。(多态你会天天用,到处用!!!!)
多态在开发中的作用是:降低程序的耦合度,提高程序的扩展力。
public class Master{
public void feed(Dog d){}
public void feed(Cat c){}
}
以上的代码中表示:Master和Dog以及Cat的关系很紧密(耦合度高)。导致扩展力很差。
public class Master{
public void feed(Pet pet){
pet.eat();
}
}
以上的代码中表示:Master和Dog以及Cat的关系就脱离了,Master关注的是Pet类。这样Master和Dog以及Cat的耦合度就降低了,提高了软件的扩展性。
面向对象的三大特征:
封装、继承、多态
真的是一环扣一环。
有了封装,有了这种整体的概念之后。
对象和对象之间产生了继承。
有了继承之后,才有了方法的覆盖和多态。
这里提到了一个软件开发原则:
七大原则最基本的原则:OCP(对扩展开放,对修改关闭)
目的是:降低程序耦合度,提高程序扩展力。
面向抽象编程,不建议面向具体编程。
2、解释之前遗留的问题
私有方法无法覆盖。
方法覆盖只是针对于“实例方法”,“静态方法覆盖”没有意义。(这是因为方法覆盖通常和多态联合起来)
总结两句话:
私有不能覆盖。
静态不谈覆盖。
在方法覆盖中,关于方法的返回值类型。
什么条件满足之后,会构成方法的覆盖呢?
1、发生具有继承关系的两个类之间。
2、父类中的方法和子类重写之后的方法:
具有相同的方法名、相同的形式参数列表、相同的返回值类型。
学习了多态机制之后:
“相同的返回值类型”可以修改一下吗?
对于返回值类型是基本数据类型来说,必须一致。
对于返回值类型是引用数据类型来说,重写之后返回值类型可以变的更小(但意义不大,实际开发中没人这样写)。
静态方法不存在方法覆盖:
/*
1、方法覆盖需要和多态机制联合起来使用才有意义。
Animal a = new Cat();
a.move();
要的是什么效果?
编译的时候move()方法是Animal的。
运行的时候自动调用到子类重写move()方法上。
假设没有多态机制,只有方法覆盖机制,你觉得有意义吗?
没有多态机制的话,方法覆盖可有可无。
没有多态机制,方法覆盖也可以没有,如果父类的方法无法满足
子类业务需求的时候,子类完全可以定义一个全新的方法。
方法覆盖和多态不能分开。
2、静态方法存在方法覆盖吗?
多态自然就和对象有关系了。
而静态方法的执行不需要对象。
所以,一般情况下,我们会说静态方法“不存在”方法覆盖。
不探讨静态方法的覆盖。
*/
public class OverrideTest05{
public static void main(String[] args){
// 静态方法可以使用“引用.”来调用吗?可以
// 虽然使用“引用.”来调用,但是和对象无关。
Animal a = new Cat(); //多态
// 静态方法和对象无关。
// 虽然使用“引用.”来调用。但是实际运行的时候还是:Animal.doSome()
a.doSome();
Animal.doSome();
Cat.doSome();
}
}
class Animal{
// 父类的静态方法
public static void doSome(){
System.out.println("Animal的doSome方法执行!");
}
}
class Cat extends Animal{
// 尝试在子类当中对父类的静态方法进行重写
public static void doSome(){
System.out.println("Cat的doSome方法执行!");
}
}
私有方法不能覆盖:
// 经过测试,你记住就行。
// 私有方法不能覆盖。
public class OverrideTest06{
// 私有方法
private void doSome(){
System.out.println("OverrideTest06's private method doSome execute!");
}
// 入口
public static void main(String[] args){
// 多态
OverrideTest06 ot = new T();
ot.doSome(); //OverrideTest06's private method doSome execute!
}
}
/*
// 在外部类中无法访问私有的。
class MyMain{
public static void main(String[] args){
OverrideTest06 ot = new T();
//错误: doSome() 在 OverrideTest06 中是 private 访问控制
//ot.doSome();
}
}
*/
// 子类
class T extends OverrideTest06{
// 尝试重写父类中的doSome()方法
// 访问权限不能更低,可以更高。
public void doSome(){
System.out.println("T's public doSome method execute!");
}
}
public class OverrideTest07{
public static void main(String[] args){
// 一般重写的时候都是复制粘贴。不要动。不要改。
}
}
class Animal{
/*
public double sum(int a, int b){
return a + b;
}
*/
/*
public long sum(int a, int b){
return a + b;
}
*/
/*
public int sum(int a, int b){
return a + b;
}
*/
}
class Cat extends Animal{
// 重写
// 错误: Cat中的sum(int,int)无法覆盖Animal中的sum(int,int)
/*
public int sum(int a, int b){
return a + b;
}
*/
/*
public double sum(int a, int b){
return a + b;
}
*/
//错误: Cat中的sum(int,int)无法覆盖Animal中的sum(int,int)
/*
public long sum(int a, int b){
return a + b;
}
*/
}
// 父类
class MyClass1{
public Animal getAnimal(){
return null;
}
}
// 子类
class MyClass2 extends MyClass1{
// 重写父类的方法
/*
public Animal getAnimal(){
return null;
}
*/
// 重写的时候返回值类型由Animal变成了Cat,变小了。(可以,java中允许)
/*
public Cat getAnimal(){
return null;
}
*/
// 重写的时候返回值类型由Animal变成了Object。变大了。(不行,java中不允许)
/*
public Object getAnimal(){
return null;
}
*/
}
13.8.1 方法覆盖代码
/*
当前程序存在的问题(设计上的问题)????
鸟儿在执行move()方法的时候,最好输出的结果是:“鸟儿在飞翔”
但是当前的程序在执行move()方法的时候输出的结果是:"动物在移动!"
很显然Bird子类从Animal父类中继承过来的move()方法已经无法满足子类的业务需求。
*/
public class OverrideTest01{
public static void main(String[] args){
// 创建鸟对象
Bird b = new Bird();
// 让鸟儿移动
b.move();
// 创建Cat类型对象
Cat c = new Cat();
c.move();
}
}
// 父类
class Animal{
// 移动
public void move(){
System.out.println("动物在移动!");
}
}
// 子类
class Bird extends Animal{
// 子类继承父类中,有一些“行为”可能不需要改进,有一些“行为”可能面临着必须改进。
// 因为父类中继承过来的方法已经无法满足子类的业务需求。
// 鸟儿在移动的时候希望输出鸟儿在飞翔!!!!
}
class Cat extends Animal{
// 猫在移动的时候,我希望输出:猫在走猫步!!!!!!
}
这里还有几个注意事项:(这几个注意事项,当学习了多态语法之后自然就明白了!)
注意1:方法覆盖只是针对于方法,和属性无关。
注意2:私有方法无法覆盖。
注意3:构造方法不能被继承,所以构造方法也不能被覆盖。
注意4:方法覆盖只是针对于“实例方法”,“静态方法覆盖”没有意义。
/*
回顾一下方法重载!!!!
什么时候考虑使用方法重载overload?
当在一个类当中,如果功能相似的话,建议将名字定义的一样,这样
代码美观,并且方便编程。
什么条件满足之后能够构成方法重载overload?
条件一:在同一个类当中
条件二:方法名相同
条件三:参数列表不同(个数、顺序、类型)
--------------------------------------------------------------------------------
什么时候我们会考虑使用“方法覆盖”呢?
子类继承父类之后,当继承过来的方法无法满足当前子类的业务需求时,
子类有权利对这个方法进行重新编写,有必要进行“方法的覆盖”。
方法覆盖又叫做:方法重写(重新编写),英语单词叫做:Override、Overwrite,都可以。
比较常见的:方法覆盖、方法重写、override
重要结论:
当子类对父类继承过来的方法进行“方法覆盖”之后,
子类对象调用该方法的时候,一定执行覆盖之后的方法。
当我们代码怎么编写的时候,在代码级别上构成了方法覆盖呢?
条件一:两个类必须要有继承关系。
条件二:重写之后的方法和之前的方法具有:
相同的返回值类型、
相同的方法名、
相同的形式参数列表。
条件三:访问权限不能更低,可以更高。(这个先记住。)
条件四:重写之后的方法不能比之前的方法抛出更多的异常,可以更少。(这个先记住)
这里还有几个注意事项:(这几个注意事项,当学习了多态语法之后自然就明白了!)
注意1:方法覆盖只是针对于方法,和属性无关。
注意2:私有方法无法覆盖。
注意3:构造方法不能被继承,所以构造方法也不能被覆盖。
注意4:方法覆盖只是针对于“实例方法”,“静态方法覆盖”没有意义。
*/
public class OverrideTest02{
public static void main(String[] args){
Bird b = new Bird();
b.move();
b.sing(1000); //Animal sing....
Cat c = new Cat();
c.move();
}
}
class Animal{
public void move(){
System.out.println("动物在移动!");
}
public void sing(int i){
System.out.println("Animal sing....");
}
}
class Bird extends Animal{
// 对move方法进行方法覆盖,方法重写,override
// 最好将父类中的方法原封不动的复制过来。(不建议手动编写)
// 方法覆盖,就是将继承过来的那个方法给覆盖掉了。继承过来的方法没了。
public void move(){
System.out.println("鸟儿在飞翔!!!");
}
//protected表示受保护的。没有public开放。
// 错误:正在尝试分配更低的访问权限; 以前为public
/*
protected void move(){
System.out.println("鸟儿在飞翔!!!");
}
*/
//错误:被覆盖的方法未抛出Exception
/*
public void move() throws Exception{
System.out.println("鸟儿在飞翔!!!");
}
*/
// 分析:这个sing()和父类中的sing(int i)有没有构成方法覆盖呢?
// 没有,原因是,这两个方法根本就是两个完全不同的方法。
// 可以说这两个方法构成了方法重载吗?可以。
public void sing(){
System.out.println("Bird sing.....");
}
}
class Cat extends Animal{
// 方法重写
public void move(){
System.out.println("猫在走猫步!!!");
}
}
//方法覆盖比较经典的案例
//一定要注意:方法覆盖/重写的时候,建议将父类的方法复制粘贴,这样比较保险。
public class OverrideTest03{
public static void main(String[] args){
// 创建中国人对象
// ChinaPeople p1 = new ChinaPeople("张三");// 错误原因:没有这样的构造方法
ChinaPeople p1 = new ChinaPeople();
p1.setName("张三");
p1.speak();
// 创建美国人对象
// AmericPeople p2 = new AmericPeople("jack"); // 错误原因:没有这样的构造方法
AmericPeople p2 = new AmericPeople();
p2.setName("jack");
p2.speak();
}
}
// 人
class People{
// 属性
private String name;
// 构造
public People(){}
public People(String name){
this.name = name;
}
//setter and getter
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
// 人都会说话
public void speak(){
System.out.println(name + "....");
}
}
// 中国人
class ChinaPeople extends People{
// 中国人说话是汉语
// 所以子类需要对父类的speak()方法进行重写
public void speak(){
System.out.println(this.getName() + "正在说汉语");
}
}
// 美国人
class AmericPeople extends People{
// 美国人说话是英语
// 所以子类需要对父类的speak()方法进行重写
public void speak(){
System.out.println(getName() + " speak english!");
}
}
覆盖toString方法
/*
关于Object类中的toString()方法
1、toString()方法的作用是什么?
作用:将“java对象”转换成“字符串的形式”。
2、Object类中toString()方法的默认实现是什么?
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
toString: 方法名的意思是转换成String
含义:调用一个java对象的toString()方法就可以将该java对象转换成字符串的表示形式。
3、那么toString()方法给的默认实现够用吗?
*/
public class OverrideTest04{
public static void main(String[] args){
// 创建一个日期对象
MyDate t1 = new MyDate();
// 调用toString()方法(将对象转换成字符串形式。)
// 问:你对这个输出结果满意吗?不满意,希望输出:xxxx年xx月xx日
// 重写MyDate的toString()方法之前的结果
//System.out.println(t1.toString()); //MyDate@28a418fc
// 重写MyDate的toString()方法之后的结果
System.out.println(t1.toString());
// 大家是否还记得:当输出一个引用的时候,println方法会自动调用引用的toString方法。
System.out.println(t1);
MyDate t2 = new MyDate(2008, 8, 8);
System.out.println(t2); //2008年8月8日
//创建学生对象
Student s = new Student(1111, "zhangsan");
// 重写toString()方法之前
//System.out.println(s); //Student@87aac27
// 重写toString()方法之后
// 输出一个学生对象的时候,可能更愿意看到学生的信息,不愿意看到对象的内存地址。
System.out.println(s.toString());
System.out.println(s);
}
}
// 日期类
class MyDate {
private int year;
private int month;
private int day;
public MyDate(){
this(1970,1,1);
}
public MyDate(int year,int month,int day){
this.year = year;
this.month = month;
this.day = day;
}
public void setYear(int year){
this.year = year;
}
public int getYear(){
return year;
}
public void setMonth(int month){
this.month = month;
}
public int getMonth(){
return month;
}
public void setDay(int day){
this.day = day;
}
public int getDay(){
return day;
}
// 从Object类中继承过来的那个toString()方法已经无法满足我业务需求了。
// 我在子类MyDate中有必要对父类的toString()方法进行覆盖/重写。
// 我的业务要求是:调用toString()方法进行字符串转换的时候,
// 希望转换的结果是:xxxx年xx月xx日,这种格式。
// 重写一定要复制粘贴,不要手动编写,会错的。
public String toString() {
return year + "年" + month + "月" + day + "日";
}
}
class Student{
int no;
String name;
public Student(int no, String name){
this.no = no;
this.name = name;
}
// 重写 方法覆盖
public String toString() {
return "学号:" + no + ",姓名:" + name;
}
}
13.8.2 多态的基础语法
多态表示多种形态:
编译的时候一种形态。
运行的时候另一种形态。
程序员要养成一个好习惯:
任何时候,任何地点,对类型进行向下转型时,一定要使用instanceof 运算符进行判断。(java规范中要求的。)这样可以很好的避免:ClassCastException
// 动物类:父类
public class Animal{
// 移动的方法
public void move(){
System.out.println("动物在移动!!!");
}
}
// 猫类,子类
public class Cat extends Animal{
// 对move方法进行重写
public void move(){
System.out.println("cat走猫步!");
}
// 猫除了move之外,应该有自己特有的行为,例如抓老鼠。
// 这个行为是子类型对象特有的方法。
public void catchMouse(){
System.out.println("猫正在抓老鼠!!!!");
}
}
// 鸟儿类,子类
public class Bird extends Animal{
// 重写父类的move方法
public void move(){
System.out.println("鸟儿在飞翔!!!");
}
// 也有自己特有的方法
public void sing(){
System.out.println("鸟儿在歌唱!!!");
}
}
// Dog并没有继承Animal
// Dog不是Animal的子类
public class Dog{
}
/*
多态的基础语法:
1、学习多态基础语法之前,我们需要普及两个概念:
第一个:向上转型
子 ---> 父(自动类型转换)
第二个:向下转型
父 ---> 子(强制类型转换,需要加强制类型转换符)
注意:
java中允许向上转型,也允许向下转型。
*****(五颗星)无论是向上转型,还是向下转型,
两种类型之间必须有继承关系,没有继承关系编译器报错。
以后在工作过程中,和别人聊天的时候,要专业一些,说
向上转型和向下转型,不要说自动类型转换,也不要说强制
类型转换,因为自动类型转换和强制类型转换是使用在基本
数据类型方面的,在引用类型转换这里只有向上和向下转型。
2、多态指的是:
父类型引用指向子类型对象。
包括编译阶段和运行阶段。
编译阶段:绑定父类的方法。
运行阶段:动态绑定子类型对象的方法。
多种形态。
3、个别同学有点混乱了:
java中只有“类名”或者“引用”才能去“点”
类名.
引用.
万变不离其宗,只要你想“点”,“点”前面要么是一个类名,要么是一个引用。
4、什么时候必须使用“向下转型”?
不要随便做强制类型转换。
当你需要访问的是子类对象中“特有”的方法。此时必须进行向下转型。
*/
public class Test01{
public static void main(String[] args){
Animal a1 = new Animal();
a1.move(); //动物在移动!!!
Cat c1 = new Cat();
c1.move(); //cat走猫步!
Bird b1 = new Bird();
b1.move(); //鸟儿在飞翔!!!
// 代码可以这样写吗?
/*
1、Animal和Cat之间有继承关系吗?有的。
2、Animal是父类,Cat是子类。
3、Cat is a Animal,这句话能不能说通?能。
4、经过测试得知java中支持这样的一个语法:
父类型的引用允许指向子类型的对象。
Animal a2 = new Cat();
a2就是父类型的引用。
new Cat()是一个子类型的对象。
允许a2这个父类型引用指向子类型的对象。
*/
Animal a2 = new Cat();
Animal a3 = new Bird();
// 没有继承关系的两个类型之间存在转型吗?
// 错误: 不兼容的类型: Dog无法转换为Animal
// Animal a4 = new Dog();
// 调用a2的move()方法
/*
什么是多态?
多种形态,多种状态。
分析:a2.move();
java程序分为编译阶段和运行阶段。
先来分析编译阶段:
对于编译器来说,编译器只知道a2的类型是Animal,
所以编译器在检查语法的时候,会去Animal.class
字节码文件中找move()方法,找到了,绑定上move()
方法,编译通过,静态绑定成功。(编译阶段属于静态绑定。)
再来分析运行阶段:
运行阶段的时候,实际上在堆内存中创建的java对象是
Cat对象,所以move的时候,真正参与move的对象是一只猫,
所以运行阶段会动态执行Cat对象的move()方法。这个过程
属于运行阶段绑定。(运行阶段绑定属于动态绑定。)
多态表示多种形态:
编译的时候一种形态。
运行的时候另一种形态。
*/
a2.move(); //cat走猫步!
// 调用a3的move()方法
a3.move(); //鸟儿在飞翔!!!
// ======================================================================
Animal a5 = new Cat(); // 底层对象是一只猫。
// 分析这个程序能否编译和运行呢?
// 分析程序一定要分析编译阶段的静态绑定和运行阶段的动态绑定。
// 只有编译通过的代码才能运行。没有编译,根本轮不到运行。
// 错误: 找不到符号
// why??? 因为编译器只知道a5的类型是Animal,去Animal.class文件中找catchMouse()方法
// 结果没有找到,所以静态绑定失败,编译报错。无法运行。(语法不合法。)
//a5.catchMouse();
// 假设代码写到了这里,我非要调用catchMouse()方法怎么办?
// 这个时候就必须使用“向下转型”了。(强制类型转换)
// 以下这行代码为啥没报错????
// 因为a5是Animal类型,转成Cat,Animal和Cat之间存在继承关系。所以没报错。
Cat x = (Cat)a5;
x.catchMouse(); //猫正在抓老鼠!!!!
// 向下转型有风险吗?
Animal a6 = new Bird(); //表面上a6是一个Animal,运行的时候实际上是一只鸟儿。
/*
分析以下程序,编译报错还是运行报错???
编译器检测到a6这个引用是Animal类型,
而Animal和Cat之间存在继承关系,所以可以向下转型。
编译没毛病。
运行阶段,堆内存实际创建的对象是:Bird对象。
在实际运行过程中,拿着Bird对象转换成Cat对象
就不行了。因为Bird和Cat之间没有继承关系。
运行时出现异常,这个异常和空指针异常一样非常重要,也非常经典:
java.lang.ClassCastException:类型转换异常。
java.lang.NullPointerException:空指针异常。这个也非常重要。
*/
//Cat y = (Cat)a6;
//y.catchMouse();
// 怎么避免ClassCastException异常的发生???
/*
新的内容,运算符:
instanceof (运行阶段动态判断)
第一:instanceof可以在运行阶段动态判断引用指向的对象的类型。
第二:instanceof的语法:
(引用 instanceof 类型)
第三:instanceof运算符的运算结果只能是:true/false
第四:c是一个引用,c变量保存了内存地址指向了堆中的对象。
假设(c instanceof Cat)为true表示:
c引用指向的堆内存中的java对象是一个Cat。
假设(c instanceof Cat)为false表示:
c引用指向的堆内存中的java对象不是一个Cat。
程序员要养成一个好习惯:
任何时候,任何地点,对类型进行向下转型时,一定要使用
instanceof 运算符进行判断。(java规范中要求的。)
这样可以很好的避免:ClassCastException
*/
System.out.println(a6 instanceof Cat); //false
if(a6 instanceof Cat){ // 如果a6是一只Cat
Cat y = (Cat)a6; // 再进行强制类型转换
y.catchMouse();
}
}
}
019-向上和向下转型
为什么要instanceof判断
public class Test03{
public static void main(String[] args){
// main方法是程序员A负责编写。
AnimalTest at = new AnimalTest();
at.test(new Cat());
at.test(new Bird());
}
}
public class AnimalTest{
// test方法是程序员B负责编写。
// 这个test()方法的参数是一个Animal
public void test(Animal a){ // 实例方法
// 你写的这个方法别人会去调用。
// 别人调用的时候可能给你test()方法传过来一个Bird
// 当然也可能传过来一个Cat
// 对于我来说,我不知道你调用的时候给我传过来一个啥。
if(a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse();
}else if(a instanceof Bird){
Bird b = (Bird)a;
b.sing();
}
}
}
13.8.3 多态在开发中的作用(★★★★★)
2.1.多态练习题
编写程序模拟“主人”喂养“宠物”的场景:
提示1:
主人类:Master
宠物类:Pet
宠物类子类:Dog、Cat、YingWu
提示2:
主人应该有喂养的方法:feed()
宠物应该有吃的方法:eat()
只要主人喂宠物,宠物就吃。
要求:主人类中只提供一个喂养方法feed(),要求达到可以喂养各种类型的宠物。
编写测试程序:
创建主人对象
创建各种宠物对象
调用主人的喂养方法feed(),喂养不同的宠物,观察执行结果。
通过该案例,理解多态在开发中的作用。
重要提示:feed方法是否需要一个参数,参数选什么类型!!!
// 宠物狗狗类
public class Dog extends Pet{
// 吃
public void eat(){
System.out.println("狗狗喜欢啃骨头,吃的很香。");
}
}
public class Cat extends Pet{
// 吃
public void eat(){
System.out.println("猫咪喜欢吃鱼,吃的很香!!!");
}
}
// 所有宠物的父类
public class Pet{
// 吃的行为(这个方法可以不给具体的实现。)
public void eat(){
}
}
public void feed(Pet pet){
// 编译的时候,编译器发现pet是Pet类,会去Pet类中找eat()方法,结果找到了,编译通过
// 运行的时候,底层实际的对象是什么,就自动调用到该实际对象对应的eat()方法上。
// 这就是多态的使用。
pet.eat();
}
// 主人类
public class Master{
/*
// 假设主人起初的时候只是喜欢养宠物狗狗
// 喂养宠物狗狗
public void feed(Dog d){
d.eat();
}
// 由于新的需求产生,导致我们“不得不”去修改Master这个类的代码
public void feed(Cat c){
c.eat();
}
*/
// 能不能让Master主人这个类以后不再修改了。
// 即使主人又喜欢养其它宠物了,Master也不需要修改。
// 这个时候就需要使用:多态机制。
// 最好不要写具体的宠物类型,这样会影响程序的扩展性。
public void feed(Pet pet){
// 编译的时候,编译器发现pet是Pet类,会去Pet类中找eat()方法,结果找到了,编译通过
// 运行的时候,底层实际的对象是什么,就自动调用到该实际对象对应的eat()方法上。
// 这就是多态的使用。
pet.eat();
}
}
/*
注意这里的分析:
主人起初的时候只喜欢养宠物狗狗
随着时间的推移,主人又喜欢上养“猫咪”
在实际的开发中这就表示客户产生了新的需求。
作为软件的开发人员来说,必须满足客户的需求。
我们怎么去满足客户的需求呢?
在不使用多态机制的前提下,目前我们只能在Master类中添加一个新的方法。
思考:软件在扩展新需求过程当中,修改Master这个类有什么问题?
一定要记住:软件在扩展过程当中,修改的越少越好。
修改的越多,你的系统当前的稳定性就越差,未知的风险就越多。
其实这里涉及到一个软件的开发原则:
软件开发原则有七大原则(不属于java,这个开发原则属于整个软件业):
其中有一条最基本的原则:OCP(开闭原则)
什么是开闭原则?
对扩展开放(你可以额外添加,没问题),对修改关闭(最好很少的修改现有程序)。
在软件的扩展过程当中,修改的越少越好。
高手开发项目不是仅仅为了实现客户的需求,还需要考虑软件的扩展性。
什么是软件扩展性?
假设电脑中的内存条部件坏了,我们可以买一个新的插上,直接使用。
这个电脑的设计就考虑了“扩展性”。内存条的扩展性很好。
面向父类型编程,面向更加抽象进行编程,不建议面向具体编程。
因为面向具体编程会让软件的扩展力很差。
*/
/*
测试多态在开发中的作用
*/
public class Test{
public static void main(String[] args){
// 创建主人对象
Master zhangsan = new Master();
// 创建宠物对象
Dog zangAo = new Dog();
// 主人喂
zhangsan.feed(zangAo);
// 创建宠物对象
Cat xiaoHua = new Cat();
// 主人喂
zhangsan.feed(xiaoHua);
// 创建宠物对象
YingWu yingWu = new YingWu();
// 主人喂
zhangsan.feed(yingWu);
}
}
/*
编写程序实现乐手弹奏乐器。乐手可以弹奏不同的乐器从而发出不同的声音。
可以弹奏的乐器包括二胡、钢琴和琵琶。
实现思路及关键代码:
1)定义乐器类Instrument,包括方法makeSound()
2)定义乐器类的子类:二胡Erhu、钢琴Piano和小提琴Violin
3)定义乐手类Musician,可以弹奏各种乐器play(Instrument i)
4)定义测试类,给乐手不同的乐器让他弹奏
*/
public class Homework{
public static void main(String[] args){
/*
// 创建各种乐器对象
Erhu erhu = new Erhu();
Piano piano = new Piano();
Violin violin = new Violin();
// 创建乐手对象
Musician musician = new Musician();
// play
musician.play(erhu);
musician.play(piano);
musician.play(violin);
*/
/*
// 创建各种乐器对象
Instrument erhu = new Erhu();
Instrument piano = new Piano();
Instrument violin = new Violin();
// 创建乐手对象
Musician musician = new Musician();
// play
musician.play(erhu);
musician.play(piano);
musician.play(violin);
*/
// 创建乐手对象
Musician musician = new Musician();
// play
musician.play(new Erhu());
musician.play(new Piano());
musician.play(new Violin());
}
}
// 第一种写法
/*
// 乐手
class Musician{
// 实例变量
Instrument i;
// 构造方法
public Musician(){
}
public Musician(Instrument i){
this.i = i;
}
// play()方法
public void play(){
i.makeSound();
}
}
*/
// 第二种写法(更符合题意)
// 乐手
class Musician{
// 乐手的名字
//private String name;
public void play(Instrument i){
// 编译阶段makeSound()方法是Instrument的。
// 运行阶段这个makeSound()方法就不一定是谁的了。
i.makeSound();
}
}
// 乐器父类
class Instrument{
// 乐器发声
public void makeSound(){
}
}
// 子类
class Erhu extends Instrument{
public void makeSound(){
System.out.println("二胡的声音!!!");
}
}
// 子类
class Piano extends Instrument{
public void makeSound(){
System.out.println("钢琴的声音!!!");
}
}
// 子类
class Violin extends Instrument{
public void makeSound(){
System.out.println("小提琴的声音!!!");
}
}
14 第十四章 super
14.1 章节目标与知识框架
14.1.1 章节目标
掌握super都可以用在哪里?理解super在内存方面的存储位置。掌握怎么通过子类的构造方法调用父类的构造方法。super什么时候可以省略,什么时候不能省略?
14.1.2 知识框架
14.2 super
14.2.1 super概述(理解)
super和this可以对比着学习:
①this
this是一个引用,保存内存地址指向自己。
this出现在实例方法中,谁调用这个实例方法,this就代表谁,this代表当前正在执行这个动作的对象。
this不能出现在静态方法中。
this大部分情况下可以省略,在方法中区分实例变量和局部变量的时候不能省略。
“this(实际参数列表)”出现在构造方法第一行,通过当前的构造方法去调用本类当中其它的构造方法。
②super
严格来说,super其实并不是一个引用,它只是一个关键字,super代表了当前对象中从父类继承过来的那部分特征。this指向一个独立的对象,super并不是指向某个“独立”的对象,假设张大明是父亲,张小明是儿子,有这样一句话:大家都说张小明的眼睛、鼻子和父亲的很像。那么也就是说儿子继承了父亲的眼睛和鼻子特征,那么眼睛和鼻子肯定最终还是长在儿子的身上。假设this指向张小明,那么super就代表张小明身上的眼睛和鼻子。换句话说super其实是this的一部分。如下图所示:张大明和张小明其实是两个独立的对象,两个对象内存方面没有联系,super只是代表张小明对象身上的眼睛和鼻子,因为这个是从父类中继承过来的,在内存方面使用了super关键字进行了标记,对于下图来说“this.眼睛”和“super.眼睛”都是访问的同一块内存空间。
super和this都可以使用在实例方法当中。
super不能使用在静态方法当中,因为super代表了当前对象上的父类型特征,静态方法中没有this,肯定也是不能使用super的。
super也有这种用法:“super(实际参数列表);”,这种用法是通过当前的构造方法调用父类的构造方法。
接下来,我们来测试一下:
编译报错了:
通过以上的测试,可以看出this是可以单独使用的引用,但super无法输出,编译器提示super要使用必须是“super.xxx”,显然super并不指向独立的对象,并不是保存某个对象的内存地址。
再来看另外的一个测试:
编译报错了:
通过以上的测试,可以看出this和super都是无法使用在静态方法当中的。
14.2.2 super使用在构造方法中(掌握)
super使用在构造方法中,语法格式为:super(实际参数列表),这行代码和“this(实际参数列表)”都是只允许出现在构造方法第一行(这一点记住就行了),所以这两行代码是无法共存的。“super(实际参数列表)”这种语法表示子类构造方法执行过程中调用父类的构造方法。我们来看一段代码:
运行结果如下图所示:
我们把上面的代码片段拿过来放在一起看看:
父类的构造方法:
子类的构造方法:
你有没有察觉到子类的构造方法前三行代码和父类构造方法中的代码是一样的?接下来把子类的构造方法修改一下,然后再运行测试程序:
运行结果如下图所示:
通过以上代码的学习,“super(实际参数列表);”语法表示调用父类的构造方法,代码复用性增强了,另外一方面也是模拟现实世界当中的“要想有儿子,必须先有父亲”的道理。不过这里的“super(实际参数列表)”在调用父类构造方法的时候,从本质上来说并不是创建一个“独立的父类对象”,而是为了完成当前对象的父类型特征的初始化操作。(或者说通过子类的构造方法调用父类的构造方法,是为了让张小明身上长出具有他父亲特点的鼻子和眼睛,鼻子和眼睛初始化完毕之后,具有父亲的特点,但最终还是长在张小明的身上)。
接下来,再来看一段代码:
运行结果如下图所示:
通过以上运行结果可以得出以下的等效代码:
运行结果如下图所示:
通过以上代码的测试我们得出,当一个构造方法第一行没有显示的调用“super(实际参数列表)”的话,系统默认调用父类的无参数构造方法“super()”。当然前提是“this(实际参数列表)”也没有显示的去调用(因为super()和this()都只能出现在构造方法第一行,所以不能并存)。我们可以通过以下程序再次测试一下:
编译报错了:
以上程序为什么会编译报错呢?原因是B类的构造方法第一行默认会调用“super()”,而super()会调用父类A的无参数构造方法,但由于父类A中提供了有参数构造方法,导致无参数构造方法不存在,从而编译报错了。所以在实际开发中还是建议程序员将无参数构造方法显示的定义出来,这样就可以避免对象的创建失败了。
另外,通过以上内容的学习,还可以得出这样的结论:在java语言当中无论是创建哪个java对象,老祖宗Object类中的无参数构造方法是必然执行的。
接下来我们再来看一下:一个java对象在创建过程中比较完整的内存图是如何变化的,请先看以下代码:
运行结果如下图所示:
以上程序创建Worker对象时构造方法的执行顺序是:
①先执行Object类的无参数构造方法;
②再执行People类的构造方法;
③最后执行Worker类的构造方法;
注意:虽然执行了三个构造方法,但是对象实际上只创建了一个Worker。以上程序的内存结构图是这样变化的:
通过以上内容的学习,super()的作用主要是:第一,调用父类的构造方法,使用这个构造方法来给当前子类对象初始化父类型特征;第二,代码复用。
14.2.3 super使用在实例方法中(掌握)
super和this都可以使用在实例方法中,并且都不能使用在静态方法当中,“this”大部分情况下都是可以省略的,只有在方法中区分局部变量和实例变量的时候不能省略。那“super”什么时候可以省略,什么时候不能省略呢?
运行结果如下图所示:
我们发现printName()方法中的super.name和this.name最终输出结果是一样的,这是为什么呢?请看以上程序执行的内存图:
通过以上内存结构图发现this.name和super.name实际上是同一块内存空间,所以它们的输出结果是完全一样的。接下来,我们再把以上的PaperBook类修改一下:
运行结果如下图所示:
为什么super.name是null呢,我们一起来看看以上程序的内存图:
通过以上内存图可以清楚的看到,父类Book的构造方法在执行的时候给super.name赋值null,子类PaperBook的构造方法在执行的时候给this.name赋值“零基础学Java卷I”,由于在子类PaperBook中定义了重名的变量name导致在当前对象中有两个name,一个是从父类中继承过来的,一个是自己的,如果此时想访问父类中继承过来的name则必须使用super.name,当直接访问name或者this.name都表示访问当前对象自己的name。
通过以上的学习,大家知道super在什么情况下不能省略了吗?当父类中有该实例变量,子类中又重新定义了同名的实例变量,如果想在子类中访问父类的实例变量,super不能省略。实例方法是这样吗?我们可以来测试一下,请看代码:
运行结果如下图所示:
通过以上测试得出最终结论:父类和子类中有同名实例变量或者有同名的实例方法,想在子类中访问父类中的实例变量或实例方法,则super是不能省略的,其它情况都可以省略。
14.3 章节小结
通过本章节内容的学习,主要理解super代表什么,主要掌握super关键字的用法,例如在实例方法中super怎么用,在构造方法中super又应该怎么用。super和this可以对比学习。
14.4 难点解惑
Java中super存储的是一个父对象的内存地址吗?this保存了内存地址指向了当前对象,那么super也是保存了内存地址指向了当前对象的父对象吗?
这个理解是错误的,在Java程序中创建Java对象的时候会调用构造方法,在构造方法执行之前会先调用父类的构造方法,在这里说明一下,调用父类的构造方法实际上并不是创建父类对象,只是为了完成初始化当前子类对象的父类型特征。所以严格意义上来说super并不指向任何对象,super只是代表了当前对象中的那部分父类型特征,单独输出super,例如System.out.println(super);是无法编译的。
14.5 章节习题
第一题:请判断以下代码的输出结果
14.6 习题答案
第一题答案:
14.7 day18课堂笔记
3、super关键字
super能出现在实例方法和构造方法中。
super的语法是:“super.”、“super()”
super不能使用在静态方法中。
super. 大部分情况下是可以省略的。
super.什么时候不能省略呢?
父类和子类中有同名属性,或者说有同样的方法,想在子类中访问父类的,super. 不能省略。
super() 只能出现在构造方法第一行,通过当前的构造方法去调用“父类”中的构造方法,目的是:创建子类对象的时候,先初始化父类型特征。super的使用:
super.属性名 【访问父类的属性】
super.方法名(实参) 【访问父类的方法】
super(实参) 【调用父类的构造方法】
14.7.1 day18代码
注意:
以后写代码的时候,一个类的无参数构造方法还是建议大家手动的写出来。
如果无参数构造方法丢失的话,可能会影响到“子类对象的构建”。
/*
1、super是一个关键字,全部小写。
2、super和this对比着学习。
this:
this能出现在实例方法和构造方法中。
this的语法是:“this.”、“this()”
this不能使用在静态方法中。
this. 大部分情况下是可以省略的。
this.什么时候不能省略呢? 在区分局部变量和实例变量的时候不能省略。
public void setName(String name){
this.name = name;
}
this() 只能出现在构造方法第一行,通过当前的构造方法去调用“本类”中
其它的构造方法,目的是:代码复用。
super:
super能出现在实例方法和构造方法中。
super的语法是:“super.”、“super()”
super不能使用在静态方法中。
super. 大部分情况下是可以省略的。
super.什么时候不能省略呢? ???????
super() 只能出现在构造方法第一行,通过当前的构造方法去调用“父类”中
的构造方法,目的是:创建子类对象的时候,先初始化父类型特征。
3、super()
表示通过子类的构造方法调用父类的构造方法。
模拟现实世界中的这种场景:要想有儿子,需要先有父亲。
4、重要的结论:
当一个构造方法第一行:
既没有this()又没有super()的话,默认会有一个super();
表示通过当前子类的构造方法调用父类的无参数构造方法。
所以必须保证父类的无参数构造方法是存在的。
5、注意:
this()和super() 不能共存,它们都是只能出现在构造方法第一行。
6、无论是怎样折腾,父类的构造方法是一定会执行的。(百分百的。)
*/
public class SuperTest01{
public static void main(String[] args){
// 创建子类对象
/*
A类的无参数构造方法!
B类的无参数构造方法!
*/
new B();
}
}
class A extends Object{
// 建议手动的将一个类的无参数构造方法写出来。
public A(){
//super(); // 这里也是默认有这一行代码的。
System.out.println("A类的无参数构造方法!");
}
// 一个类如果没有手动提供任何构造方法,系统会默认提供一个无参数构造方法。
// 一个类如果手动提供了一个构造方法,那么无参数构造系统将不再提供。
public A(int i){
//super();
System.out.println("A类的有参数构造方法(int)");
}
}
class B extends A{
/*
public B(){
super();
System.out.println("B类的无参数构造方法!");
}
*/
public B(){
this("zhangsan");
// 调用父类中有参数的构造方法
//super(123);
System.out.println("B类的无参数构造方法!");
}
public B(String name){
super();
System.out.println("B类的有参数构造方法(String)");
}
}
/*
判断程序的输出结果
1
3
6
5
4
在java语言中不管是是new什么对象,最后老祖宗的Object类的无参数构造方法
一定会执行。(Object类的无参数构造方法是处于“栈顶部”)
栈顶的特点:
最后调用,但是最先执行结束。
后进先出原则。
大家要注意:
以后写代码的时候,一个类的无参数构造方法还是建议大家手动的写出来。
如果无参数构造方法丢失的话,可能会影响到“子类对象的构建”。
*/
public class SuperTest02{
public static void main(String[] args){
new C();//13654
}
}
/*
class Object{
public Object(){
}
}
*/
class A extends Object{
public A(){
System.out.println("1"); //1
}
}
class B extends A{
public B(){
System.out.println("2"); //2
}
public B(String name){
super();
System.out.println("3"); // 3
}
}
class C extends B{
public C(){ // 这个是最先调用的。但是最后结束。
this("zhangsan");
System.out.println("4");//4
}
public C(String name){
this(name, 20);
System.out.println("5");//5
}
public C(String name, int age){
super(name);
System.out.println("6");//6
}
}
super(实参)的作用是:初始化当前对象的父类型特征。并不是创建新对象。实际上对象只创建了1个。
super关键字代表的就是“当前对象”的那部分父类型特征。
/*
1、举个例子:在恰当的时间使用:super(实际参数列表);
2、注意:在构造方法执行过程中一连串调用了父类的构造方法,
父类的构造方法又继续向下调用它的父类的构造方法,但是实际上
对象只创建了一个。
3、思考:“super(实参)”到底是干啥的?
super(实参)的作用是:初始化当前对象的父类型特征。
并不是创建新对象。实际上对象只创建了1个。
4、super关键字代表什么呀?
super关键字代表的就是“当前对象”的那部分父类型特征。
我继承了我父亲的一部分特征:
例如:眼睛、皮肤等.
super代表的就是“眼睛、皮肤等”。
“眼睛、皮肤等”虽然是继承了父亲的,但这部分是在我身上呢。
*/
// 测试程序
public class SuperTest03{
public static void main(String[] args){
CreditAccount ca1 = new CreditAccount();
System.out.println(ca1.getActno() + "," + ca1.getBalance() + "," + ca1.getCredit());
CreditAccount ca2 = new CreditAccount("1111", 10000.0, 0.999);
System.out.println(ca2.getActno() + "," + ca2.getBalance() + "," + ca2.getCredit());
}
}
// 账户
class Account extends Object{
// 属性
private String actno;
private double balance;
// 构造方法
public Account(){
//super();//隐藏的super()
//this.actno = null;//隐藏的代码,在没有给属性赋初值时执行
//this.balance = 0.0;//隐藏的代码,在没有给属性赋初值时执行
}
public Account(String actno, double balance){
// super();
this.actno = actno;
this.balance = balance;
}
// setter and getter
public void setActno(String actno){
this.actno = actno;
}
public String getActno(){
return actno;
}
public void setBalance(double balance){
this.balance = balance;
}
public double getBalance(){
return balance;
}
}
// 信用账户
class CreditAccount extends Account{
// 属性:信誉度(诚信值)
// 子类特有的一个特征,父类没有。
private double credit;
// 构造方法
// 分析以下程序是否存在编译错误????
public CreditAccount(String actno, double balance, double credit){
// 私有的属性,只能在本类中访问。
/*
this.actno = actno;
this.balance = balance;
*/
// 以上两行代码在恰当的位置,正好可以使用:super(actno, balance);
// 通过子类的构造方法调用父类的构造方法。
super(actno, balance);
this.credit = credit;
}
// 提供有参数的构造方法
public CreditAccount(){
//super();//隐藏的super()
//this.credit = 0.0;//隐藏的代码,在没有给属性赋初值时执行
}
// setter and getter方法
public void setCredit(double credit){
this.credit = credit;
}
public double getCredit(){
return credit;
}
}
021-super的原理
public class SuperTest04{
public static void main(String[] args){
Vip v = new Vip("张三");
v.shopping();
}
}
class Customer{
String name;
public Customer(){}
public Customer(String name){
super();
this.name = name;
}
}
class Vip extends Customer{
public Vip(){}
public Vip(String name){
super(name);
}
// super和this都不能出现在静态方法中。
public void shopping(){
// this表示当前对象。
System.out.println(this.name + "正在购物!");
// super表示的是当前对象的父类型特征。(super是this指向的那个对象中的一块空间。)
System.out.println(super.name + "正在购物!");
System.out.println(name + "正在购物!");
}
}
020-super关键字的理解
/*
1、“this.”和“super.”大部分情况下都是可以省略的。
2、this. 什么时候不能省略?
public void setName(String name){
this.name = name;
}
3、super. 什么时候不能省略?
父中有,子中又有,如果想在子中访问“父的特征”,super. 不能省略。
*/
public class SuperTest05{
public static void main(String[] args){
Vip v = new Vip("张三");
v.shopping();
}
}
class Customer {
String name;
public Customer(){}
public Customer(String name){
super();
this.name = name;
}
public void doSome(){
System.out.println(this.name + " do some!");
System.out.println(name + " do some!");
//错误: 找不到符号
//System.out.println(super.name + " do some!");
}
}
class Vip extends Customer{
// 假设子类也有一个同名属性
// java中允许在子类中出现和父类一样的同名变量/同名属性。
String name; // 实例变量
public Vip(){
}
public Vip(String name){
super(name);
// this.name = null;
}
public void shopping(){
/*
java是怎么来区分子类和父类的同名属性的?
this.name:当前对象的name属性
super.name:当前对象的父类型特征中的name属性。
*/
System.out.println(this.name + "正在购物!"); // null 正在购物
System.out.println(super.name + "正在购物!"); // 张三正在购物
System.out.println(name + "正在购物!"); //null 正在购物
}
}
023-SuperTest05
/*
通过这个测试得出的结论:
super 不是引用。super也不保存内存地址,super也不指向任何对象。
super 只是代表当前对象内部的那一块父类型的特征。
*/
public class SuperTest06 {
// 实例方法
public void doSome(){
// SuperTest06@2f92e0f4
System.out.println(this);
// 输出“引用”的时候,会自动调用引用的toString()方法。
//System.out.println(this.toString());
//编译错误: 需要'.'
//System.out.println(super);
}
// this和super不能使用在static静态方法中。
/*
public static void doOther(){
System.out.println(this);
System.out.println(super.xxx);
}
*/
// 静态方法,主方法
public static void main(String[] args){
SuperTest06 st = new SuperTest06();
st.doSome();
// main方法是静态的
// 错误的。
/*
System.out.println(this);
System.out.println(super.xxxx);
*/
}
}
/*
在父和子中有同名的属性,或者说有相同的方法,
如果此时想在子类中访问父中的数据,必须使用“super.”加以区分。
super.属性名 【访问父类的属性】
super.方法名(实参) 【访问父类的方法】
super(实参) 【调用父类的构造方法】
*/
public class SuperTest07{
public static void main(String[] args){
/*
Cat move!
Cat move!
Animal move!
*/
Cat c = new Cat();
c.yiDong();
}
}
class Animal{
public void move(){
System.out.println("Animal move!");
}
}
class Cat extends Animal{
// 对move进行重写。
public void move(){
System.out.println("Cat move!");
}
// 单独编写一个子类特有的方法。
public void yiDong(){
this.move();
move();
// super. 不仅可以访问属性,也可以访问方法。
super.move();
}
}
00 IDEA工具的使用
1、关于java的集成开发环境:
eclipse、IntelliJ IDEA等。
其中目前主流的集成开发环境是:IntelliJ IDEA
这只是一个工具,不要让一个工具把你难住了。
开发工具不要使用汉化版,太low。
英语单词太多别害怕,记位置。(一共就那几个主要的操作位置。)
2、安装IDEA工具
自己课下按照破解文档进行破解。
3、IDEA工具的使用:
第一次打开的时候:会弹出一个窗口(import idea settings)
这个表示导入idea的设置,但我们是第一次使用idea工具,没有设置过idea,所以这里选择:do not import setting...不导入设置。
第二步:会让你接受条款,接受即可。
第三步:don't send
第四步:一直下一步(最终选择免费试用30天。)
第五步:可能会让你填写email等信息,这里不填写,继续continue。
第六步:弹出welcome窗口
点击create new project
注意:在IDEA当中一个project相当于eclipse当中的一个workspace。
第七步:新建一个Empty Project
新建一个空的工程,选择创建工程窗口下面“最后的那一项”,Empty Project
第八步:给空的工程起一个名字:javase
存储到:C:\Users\Administrator\IdeaProjects\javase
点击finish。
第九步:自动弹出一个每日提示,这个每日提示可以取消掉。以后每一次打开就不再提示了。
第十步:会自动弹出一个:project structure,这个窗口先取消掉。
第十一步:在空的工程下新建Module(模块),IDEA中模块类似于eclipse当中的project。
eclipse的组织方式:
workspace--> project
idea的组织方式:
project --> module
怎么创建module?
file菜单-->new --> Module
第十二步:在New Module窗口上点击左上角的java,然后next
第十三步:给module起一个名字:chapter15
第十四步:编写代码,在src目录下新建类,写代码,并运行。
4、关于IDEA工具的快捷键以及一些简单的设置
4.1、字体设置
file --> settings --> 输入font --> 设置字体样式以及字号大小。
4.2、快速生成main方法
psvm
4.3、快速生成System.out.println()
sout
4.4、注意:IDEA是自动保存,不需要ctrl + s
4.5、删除一行
ctrl + y
4.6、怎么运行:
代码上右键-->run
或者点击左侧的绿色箭头。
ctrl + shift + F10
4.7、左侧窗口中的列表怎么展开?怎么关闭?
左箭头关闭。
右箭头展开。
上下箭头移动。
4.8、idea中退出任何窗口,都可以使用esc键盘。(esc就是退出)
4.9、任何新增/新建/添加的快捷键是:
alt + insert
4.10、窗口变大,变小:
ctrl + shift + F12
4.11、切换java程序:从HelloWorld切换到User
alt + 右箭头
或者
alt + 左箭头
4.12、切换窗口:
alt + 标号
alt + 1(打开,关闭)
alt + 2
4.13、提示方法的参数:ctrl + p
4.14、注释:
单行注释:ctrl + /
多行注释:ctrl + shift + /
4.15、idea中怎么定位方法/属性/变量?
光标停到某个单词的下面,这个单词可能是:
方法名、变量名
停到单词下面之后,按ctrl键,出现下划线,点击跳转。
4.16、idea当中复制一行是ctrl + d
4.17、idea中纠正错误的快捷键:alt+回车
4.18、快速定位类中的方法:Ctrl+F12
01 面向对象
1.1 final关键字
final表示不可改变的含义
采用final修饰的类不能被继承
采用final修饰的方法不能被覆盖
采用final修饰的变量不能被修改
final修饰的变量必须显示初始化
如果修饰的引用,那么这个引用只能指向一个对象,也就是说这个引用不能再次赋值,但被指向的对象是可以修改的
构造方法不能被final修饰
会影响JAVA类的初始化:final定义的静态常量调用时不会执行java的类初始化方法,也就是说不会执行static代码块等相关语句,这是由java虚拟机规定的。我们不需要了解的很深,有个概念就可以了。
1.1.1 final修饰的类、方法、局部变量
/*
final
1、final是java语言中的一个关键字。
2、final表示:
最终的,不可变的。
3《重点》、final修饰的变量?
final修饰的局部变量,一旦赋值不能重新赋值。
4《重点》、final修饰的方法?
final修饰的方法无法被覆盖,被重写。
5《重点》、final修饰的类?
final修饰的类无法继承。
6《提示》、final控制不了能不能调用的问题。final管的是啥?
final修饰的表示最后的,不能变的,不能改的。
重点【精辟:final修饰的变量,只能赋一次值。】
*/
public class FinalTest01{
public static void main(String[] args){
//局部变量
int i =100;
//重新赋值
i = 200;
//局部变量
final int k = 100;
//重新赋值
//错误:无法为最终变量k分配值
//k = 300;
final int m;
// 第一次赋值
m = 200;
// 重新赋值
// 编译报错。
// m = 300;
}
}
final class A{ // A没有子孙
}
//B类继承A类,相当于对A类的功能进行扩展。如果你不希望别人对A类型进行扩展。
//你可以给A类加final关键字,这样的话A类就无法继承了。
//错误:无法从最终A进行继承
/*
class B extend A{
}
*/
//错误:无法从最终String进行继承
/*
class MyString extends String{
}
*/
class C{
public final void doSome(){
System.out.println("C's doSome...");
}
}
class D extends C{
/*
public void doSome(){
System.out.println("D's doSome...");
}
*/
public final void doOther(){
}
public static void main(String[] args){
//多态
C c = new D();
//c.doOther(); // 报错了,因为编译器报错,编译器认为c是C类,C类中没有doOther方法
//调用子类中特有的方法时,需要向下转型。
if(c instanceof D){
D d1 = (D)c;
d1.doOther();
}
//不用多态可以。
D d = new D();
d.doOther();
}
}
1.1.2 final修饰引用
/*
final修饰的变量,如果这个变量是一个“引用”会怎样???
《重点》final修饰的变量只能赋一次值(万变不离其宗)
“引用”是不是一个变量呢???是。
final修饰的引用:
该引用只能指向1个对象,并且它只能永远指向该对象,无法再指向其它对象。
并且在该方法执行过程中,该引用指向对象之后,该对象不会被垃圾回收器回收。
直到当前方法结束,才会释放空间。
虽然final的引用指向对象A后,不能再重新指向对象B。
但是对象A内部的数据可以被修改。
*/
public class FinalTest02{
public static void main(String[] args){
Person p1 = new Person(20);
System.out.println(p1.age);
//----------------------------------------------------------
//代码不管怎么变化,p也是一个变量。(只不过这里它有一个特殊的名字:引用)
final Person p = new Person(30);
// 错误:无法为最终变量p分配值
//p = new Person(30);
// 可以给p一个null吗?
// 错误:无法为最终变量p分配值
// p = null;
p.age = 40;
System.out.println(p.age);
/*
final int i = 10;
// 不允许的
i = 20;
*/
}
}
class Person{
int age;
public Person(){
}
public Person(int age){
this.age = age;
}
}
002-final修饰的引用
1.1.3 final修饰实例变量
实际开发中代码一般不这样编写,final修饰实例变量一般会和static联合修饰,避免浪费堆内存空间。static final联合修饰的变量称为“常量”
/*
final修饰的实例变量呢?
《重点:万变不离其宗》:final修饰的变量只能赋值一次。(这句话到哪里都好使)
你是否还记得:实例变量如果没有手动赋值的话,系统会赋默认值。
java程序设计:不背锅!!!!甩锅甩的很好!!!!
实例变量在什么时候赋值(初始化)?
构造方法执行的过程中赋值。(new的时候赋值)
终极结论:
final修饰的实例变量,系统不负责赋默认值,要求程序员必须手动赋值。
这个手动赋值,在变量后面赋值可以,在构造方法中赋值也可以。
*/
public class FinalTest03{
public static void main(String[] args){
// 创建对象必须调用无参构造方法吗?
// 不一定
User user = new User(111.0);
User user2 = new User(0.0);
}
}
class User{
// 实例变量
// 编译器报错
//final int age;
// 实例变量
// 可以,因为程序员手动赋值了。
final double height = 1.8;
// 以下这一堆代码全部联合起来,weight变量也是赋值了1次。
// 实例变量
final double weight;
// 构造方法
/*
public User(){
System.out.println("Hello World!");
this.weight = 80; // 只要我赶在系统赋默认值之前赋值就行。
//this.weight = 81; // 这个可以吗?不行。
}
*/
public User(double d){
// 这也是赋值了,没有采用系统默认值
this.weight = d;
}
}
1.1.4 常量
/*
上一个例子的结论:
final修饰的实例变量,必须手动赋值。
《重点》final修饰的变量,只能赋一次值。
final修饰的实例变量一般添加static修饰。
终极结论:
static final联合修饰的变量成为“常量”。
常量名建议全部大写,每个单词之间采用下划线衔接。
常量:实际上常量和静态变量一样,区别在于:
常量的值不能变。
常量和静态变量,都是存储在方法区,并且都是在类加载时初始化。
常量一般都是公共的:public的。
*/
public class FinalTest04{
public static void main(String[] args){
System.out.println(Chinese.COUNTRY);
//错误:无法为最终变量COUNTRY分配值
//Chinese.COUNTRY = "美国"; // 常量是无法重新赋值的。
}
}
class Chinese{
// 身份证号,每个人都不一样,对象级别的
String idCard;
// 姓名,对象不同姓名不一样。
String name;
// 国家的值是一个固定值:“中国”
// 实例变量在堆中,一个对象一份。100个对象100份。
// 实例变量既然使用final修饰了,说明该实例变量值不会随着对象的变化而变化。
// 该实例变量前面应该添加:static关键字,变为静态的,存储在方法区。
//static final String country = "中国";
public static final String COUNTRY = "中国";
// i永远都是10,创建100个对象,i也是10.
// i是10是永远不会发生改变的,既然这样,没必要声明为实例变量,最好是静态的,节省内存。
static final int i = 10;
}
class MyMath{
// 数学中的π是永远不会变的。
public static final double PI = 3.1415926;
}
1.2 抽象类
看我们以前示例中的Person、Student和Employee,从我们使用的角度来看主要对Student和Employee进行实例化,Person中主要包含了一些公共的属性和方法,而Person我们通常不会实例化,所以我们可以把它定义成抽象的:
在java中采用abstract关键字定义的类就是抽象类,采用abstract关键字定义的方法就是抽象方法
抽象的方法只需在抽象类中,提供声明,不需要实现
如果一个类中含有抽象方法,那么这个类必须定义成抽象类
如果这个类是抽象的,那么这个类被子类继承,抽象方法必须被重写。如果在子类中不复写该抽象方法,那么必须将此类再次声明为抽象类
抽象的类是不能实例化的,就像现实世界中人其实是抽象的,张三、李四才是具体的
抽象类不能被final修饰
抽象方法不能被final修饰,因为抽象方法就是被子类实现的
抽象类中可以包含方法实现,可以将一些公共的代码放到抽象类中,另外在抽象类中可以定义一些抽象的方法,这样就会存在一个约束,而子类必须实现我们定义的方法,如:teacher必须实现printInfo方法,Student也必须实现printInfo方法,方法名称不能修改,必须为printInfo,这样就能实现多态的机制,有了多态的机制,我们在运行期就可以动态的调用子类的方法。所以在运行期可以灵活的互换实现。
1.2.1 抽象类概述
001-抽象类的理解
/*
类到对象是实例化。对象到类是抽象。
抽象类:
1、什么是抽象类?
类和类之间具有共同特征,将这些共同特征提取出来,形成的就是抽象类。
类本身是不存在的,所以抽象类无法创建对象《无法实例化》。
2、抽象类属于什么类型?
抽象类也属于引用数据类型。
3、抽象类怎么定义?《能把基础语法先学会》
语法:
[修饰符列表] abstract class 类名{
类体;
}
4、抽象类是无法实例化的,无法创建对象的,所以抽象类是用来被子类继承的。
5、final和abstract不能联合使用,这两个关键字是对立的。
6、抽象类的子类可以是抽象类。也可以是非抽象类。
7、抽象类虽然无法实例化,但是抽象类有构造方法,这个构造方法是供子类使用的。
8、抽象类关联到一个概念:抽象方法。什么是抽象方法呢?
抽象方法表示没有实现的方法,没有方法体的方法。例如:
public abstract void doSome();
抽象方法特点是:
特点1:没有方法体,以分号结尾。
特点2:前面修饰符列表中有abstract关键字。
9、抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中。
*/
public class AbstractTest01{
public static void main(String[] args){
// 错误: Account是抽象的; 无法实例化
//Account act = new Account();
}
}
// 银行账户类
//错误: 非法的修饰符组合: abstract和final
/*
final abstract class Account{
}
*/
abstract class Account{
/*
public Account(){
}
public Account(String s){
}
*/
// 非抽象方法
public void doOther(){
}
// 抽象方法
public abstract void withdraw();
}
// 子类继承抽象类,子类可以实例化对象
/*
class CreditAccount extends Account{
public CreditAccount(){
super();
}
}
*/
// 抽象类的子类可以是抽象类吗?可以
/*
abstract class CreditAccount extends Account{
}
*/
1.2.2 非抽象类继承抽象类必须将抽象方法实现
/*
抽象类:
1、抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中。
2、重要结论:重要结论五颗星*****(必须记住)
一个非抽象的类继承抽象类,必须将抽象类中的抽象方法实现了。
这是java语法上强行规定的,必须的,不然编译器就报错了。
这里的覆盖或者说重写,也可以叫做实现。(对抽象的实现。)
*/
public class AbstractTest02{
public static void main(String[] args){
// 能不能使用多态?
// 父类型引用指向子类型对象。
Animal a = new Bird(); // 向上转型。(自动类型转换)
// 这就是面向抽象编程。
// 以后你都是调用的a.XXXX
// a的类型是Animal,Animal是抽象的
// 面向抽象编程,不要面向具体编程,降低程序的耦合度,提高程序的扩展力。
// 这种编程思想符合OCP原则。
/*
分析以下:
编译的时候这个move()方法是谁的?
运行的时候这个move()方法又是谁的?
*/
a.move();
// 多态(当对多态不是很理解的时候,以后写代码能用多态就用多态。慢慢就理解了。)
Animal x = new Cat();
x.move();
}
}
// 动物类(抽象类)
abstract class Animal{
// 抽象方法
public abstract void move();
}
// 子类(非抽象的)
// 错误: Bird不是抽象的, 并且未覆盖Animal中的抽象方法move()
/*
class Bird extends Animal{
}
*/
class Bird extends Animal{
// 需要将从父类中继承过来的抽象方法进行覆盖/重写,或者也可以叫做“实现”。
// 把抽象的方法实现了。
public void move(){
System.out.println("鸟儿在飞翔!");
}
}
class Cat extends Animal{
public void move(){
System.out.println("猫在走猫步!");
}
}
// 如果Bird是抽象类的话,那么这个Animal中继承过来的抽象方法也可以不去重写/覆盖/实现。
/*
abstract class Bird extends Animal{
}
*/
/*
有些内容不要死记硬背,讲讲道理。
分析:
Animal是父类,并且是 抽象的。
Animal这个抽象类中有一个抽象方法move。
Bird是子类,并且是 非抽象的。
Bird继承Animal之后,会将抽象方法继承过来。
*/
1.3 接口(行为)
接口我们可以看作是抽象类的一种特殊情况,在接口中只能定义抽象的方法和常量
1)在java中接口采用interface声明
2)接口中的方法默认都是publicabstract的,不能更改
3)接口中的变量默认都是publicstaticfinal类型的,不能更改,所以必须显示的初始化
4)接口不能被实例化,接口中没有构造函数的概念
5)接口之间可以继承,但接口之间不能实现
6)接口中的方法只能通过类来实现,通过implements关键字
7)如果一个类实现了接口,那么接口中所有的方法必须实现
8)一类可以实现多个接口
1.3.1 接口的基础语法
接口中只包含两部分内容,一部分是:常量。一部分是:抽象方法。接口中没有其它内容了。只有以上两部分。
接口中的常量的public static final可以省略。
接口中的抽象方法定义时:public abstract修饰符可以省略。
/*
接口:
1、接口也是一种“引用数据类型”。编译之后也是一个class字节码文件。
2、接口是完全抽象的。(抽象类是半抽象。)或者也可以说接口是特殊的抽象类。
3、接口怎么定义,语法是什么?
[修饰符列表] interface 接口名{}
4、接口支持多继承,一个接口可以继承多个接口。
5、接口中只包含两部分内容,一部分是:常量。一部分是:抽象方法。接口中没有其它内容了。只有以上两部分。
6、接口中所有的元素都是public修饰的。(都是公开的。)
7、接口中的抽象方法定义时:public abstract修饰符可以省略。
8、接口中的方法都是抽象方法,所以接口中的方法不能有方法体。
9、接口中的常量的public static final可以省略。
*/
public class Test01{
public static void main(String[] args){
// 访问接口的常量。
System.out.println(MyMath.PI);
// 常量能重新赋值吗?
//错误: 无法为最终变量PI分配值
//MyMath.PI = 3.1415928;
//错误: 无法为最终变量k分配值
//MyMath.k = 111;
}
}
// 定义接口
interface A{
}
// 接口支持继承
interface B extends A{
}
// 一个接口可以继承多个接口(支持多继承)
interface C extends A, B{
}
// 我的数学接口
interface MyMath{
// 常量
//public static final double PI = 3.1415926;
// public static final可以省略吗?
double PI = 3.1415926;
// k是不是常量????是。
// 接口中随便写一个变量就是常量。
// 常量:值不能发生改变的变量。
int k = 100;
// 抽象方法
//public abstract int sum(int a, int b);
// 接口当中既然都是抽象方法,那么在编写代码的时候,public abstract可以省略吗?
int sum(int a, int b);
// 接口中的方法可以有方法体吗?
// 错误: 接口抽象方法不能带有主体
/*
void doSome(){
}
*/
// 相减的抽象方法
int sub(int a, int b);
}
1.3.2 类实现接口要实现所有方法
五颗星(*****):当一个非抽象的类实现接口的话,必须将接口中所有的抽象方法全部实现(覆盖、重写)。
/*
接口的基础语法:
1、类和类之间叫做继承,类和接口之间叫做实现。
别多想:你仍然可以将"实现"看做“继承”。
继承使用extends关键字完成。
实现使用implements关键字完成。
2、五颗星(*****):当一个非抽象的类实现接口的话,必须将接口中所有的
抽象方法全部实现(覆盖、重写)。
*/
public class Test02{
public static void main(String[] args){
//错误: MyMath是抽象的; 无法实例化
//new MyMath();
// 能使用多态吗?可以。
//Animal a = new Cat();
// 父类型的引用指向子类型的对象
MyMath mm = new MyMathImpl();
// 调用接口里面的方法(面向接口编程。)
int result1 = mm.sum(10, 20);
System.out.println(result1);
int result2 = mm.sub(20, 10);
System.out.println(result2);
}
}
// 特殊的抽象类,完全抽象的,叫做接口。
interface MyMath{
double PI = 3.1415926;
int sum(int a, int b);
int sub(int a, int b);
}
// 这样没问题
/*
abstract class MyMathImpl implements MyMath {
}
*/
// 编写一个类(这个类是一个“非抽象”的类)
// 这个类的名字是随意的。
//错误: MyMathImpl不是抽象的, 并且未覆盖MyMath中的抽象方法sub(int,int)
/*
class MyMathImpl implements MyMath {
}
*/
//修正
class MyMathImpl implements MyMath {
//错误:正在尝试分配更低的访问权限; 以前为public
/*
int sum(int a, int b){
return a + b;
}
*/
// 重写/覆盖/实现 接口中的方法(通常叫做实现。)
public int sum(int a, int b){
return a + b;
}
public int sub(int a, int b){
return a - b;
}
}
1.3.3 一个类可以实现多个接口(★★★★★)
无论向上转型还是向下转型,两种类型之间必须要有继承关系,没有继承关系编译器会报错。(这句话不适用在接口方面。)
最终实际上和之前还是一样,需要加:instanceof运算符进行判断。向下转型养成好习惯。转型之前先if+instanceof进行判断。
/*
接口和接口之间支持多继承,那么一个类可以同时实现多个接口吗?
对于计算机来说,一个机箱上有多个接口,一个接口是接键盘的,
一个接口是接鼠标的,一个接口是接电源的,一个接口是接显示器的.....
重点(五颗星*****):一个类可以同时实现多个接口。
这种机制弥补了java中的哪个缺陷?
java中类和类只支持单继承。实际上单继承是为了简单而出现的,现实世界中
存在多继承,java中的接口弥补了单继承带来的缺陷。
接口A和接口B虽然没有继承关系,但是写代码的时候,可以互转。
编译器没意见。但是运行时可能出现:ClassCastException
之前有一个结论:
无论向上转型还是向下转型,两种类型之间必须要有继承关系,
没有继承关系编译器会报错。(这句话不适用在接口方面。)
最终实际上和之前还是一样,需要加:instanceof运算符进行判断。
向下转型养成好习惯。转型之前先if+instanceof进行判断。
*/
public class Test03{
public static void main(String[] args){
// 多态该怎么用呢?
// 都是父类型引用指向子类型对象
A a = new D();
//a.m2(); // 编译报错。A接口中没有m2()方法。
B b = new D();
C c = new D();
// 这个编译没问题,运行也没问题。
// 调用其他接口中的方法,你需要转型(接口转型。)
B b2 = (B)a;
b2.m2();
// 直接向下转型为D可以吗?可以
D d = (D)a;
d.m2();
M m = new E();
// 经过测试:接口和接口之间在进行强制类型转换的时候,没有继承关系,也可以强转。
// 但是一定要注意,运行时可能会出现ClassCastException异常。
// 编译没问题,运行有问题。
//K k = (K)m;
if(m instanceof K){
K k = (K)m;
}
}
}
interface K{
}
interface M{
}
class E implements M{
}
// --------------------------------------------------------------------
interface X{
}
interface Y{
}
interface Z extends X,Y{ //接口和接口支持多继承。
}
//------------------------------------------------------------------
interface A{
void m1();
}
interface B{
void m2();
}
interface C{
void m3();
}
// 实现多个接口,其实就类似于多继承。
class D implements A,B,C{
// 实现A接口的m1()
public void m1(){
}
// 实现B接口中的m2()
public void m2(){
System.out.println("m2 ....");
}
// 实现接口C中的m3()
public void m3(){
}
}
1.3.4 extends和implement同时出现
extends 关键字在前,implements 关键字在后。
/*
继承和实现都存在的话,代码应该怎么写?
extends 关键字在前。
implements 关键字在后。
*/
public class Test04{
public static void main(String[] args){
// 创建对象(表面看Animal类没起作用!)
Flyable f = new Cat(); //多态。
f.fly();
// 同一个接口
Flyable f2 = new Pig();
// 调用同一个fly()方法,最后的执行效果不同。
f2.fly();
Flyable f3 = new Fish();
f3.fly();
}
}
// 动物类:父类
class Animal{
}
// 可飞翔的接口(是一对翅膀)
// 能插拔的就是接口。(没有接口你怎么插拔。)
// 内存条插到主板上,他们之间有接口。内存条可以更换。
// 接口通常提取的是行为动作。
interface Flyable{
void fly();
}
// 动物类子类:猫类
// Flyable是一个接口,是一对翅膀的接口,通过接口插到猫身上,让猫变的可以飞翔。
class Cat extends Animal implements Flyable{
public void fly(){
System.out.println("飞猫起飞,翱翔太空的一只猫,很神奇,我想做一只猫!!");
}
}
// 蛇类,如果你不想让它飞,可以不实现Flyable接口
// 没有实现这个接口表示你没有翅膀,没有给你插翅膀,你肯定不能飞。
class Snake extends Animal{
}
// 想飞就插翅膀这个接口。
class Pig extends Animal implements Flyable{
public void fly(){
System.out.println("我是一只会飞的猪!!!");
}
}
// 鱼(默认实际上是存在继承的,默认继承Object。)
/*
class Fish extends Object implements Flyable{
}
*/
class Fish implements Flyable{ //没写extends,也是有的,默认继承Object。
public void fly(){
System.out.println("我是六眼飞鱼(流言蜚语)!!!");
}
}
1.3.5 接口在开发中的作用
/*
接口:菜单,抽象的
*/
public interface FoodMenu{
// 西红柿炒蛋
void shiZiChaoJiDan();
// 鱼香肉丝
void yuXiangRouSi();
}
//中餐厨师
// 实现菜单上的菜
// 厨师是接口的实现者。
public class ChinaCooker implements FoodMenu{
// 西红柿炒蛋
public void shiZiChaoJiDan(){
System.out.println("中餐师傅做的西红柿炒鸡蛋,东北口味!");
}
// 鱼香肉丝
public void yuXiangRouSi(){
System.out.println("中餐师傅做的鱼香肉丝,东北口味!");
}
}
//西餐厨师
// 实现菜单上的菜
// 厨师是接口的实现者。
public class AmericCooker implements FoodMenu{
// 西红柿炒蛋
public void shiZiChaoJiDan(){
System.out.println("西餐师傅做的西红柿炒鸡蛋!");
}
// 鱼香肉丝
public void yuXiangRouSi(){
System.out.println("西餐师傅做的鱼香肉丝!");
}
}
// 顾客
public class Customer{
// 顾客手里有一个菜单
// Customer has a FoodMenu!(这句话什么意思:顾客有一个菜单)
// 记住:以后凡是能够使用 has a 来描述的,统一以属性的方式存在。
// 实例变量,属性
// 面向抽象编程,面向接口编程。降低程序的耦合度,提高程序的扩展力。
private FoodMenu foodMenu;
// 如果以下这样写,就表示写死了(焊接了。没有可插拔了。)
// 中餐厨师
//ChinaCooker cc;
// 西餐厨师
//AmericCooker ac
// 构造方法
public Customer(){
}
public Customer(FoodMenu foodMenu){
this.foodMenu = foodMenu;
}
// setter and getter
public void setFoodMenu(FoodMenu foodMenu){
this.foodMenu = foodMenu;
}
public FoodMenu getFoodMenu(){
return foodMenu;
}
// 提供一个点菜的方法
public void order(){
// 先拿到菜单才能点菜
// 调用get方法拿菜单。
//FoodMenu fm = this.getFoodMenu();
// 也可以不调用get方法,因为在本类中私有的属性是可以访问
foodMenu.shiZiChaoJiDan();
foodMenu.yuXiangRouSi();
}
}
/*
Cat is a Animal,但凡满足is a的表示都可以设置为继承。
Customer has a FoodMenu,但凡是满足has a的表示都以属性的形式存在。
*/
/*
class Address{
String city;
String street;
String zipcode;
}
class User{
int id;
// 和这个一样。
// String是一个类。
// name是变量名。
// name是一个引用。
String name;
// Address是一个类名。
// 这就是一个变量。
// 实例变量。
Address addr; // addr是一个引用。是一个变量。
public static void main(String[] args){
// 局部变量
//Address addr;
//addr = new Address();
// 合并。
Address addr = new Address();
User u = new User();
u.id = 100;
u.name = "zhangsan";
u.addr = new Address();
System.out.println(u.addr.city); // null
System.out.println(u.addr.street); // null
System.out.println(u.addr.zipcode); // null
}
}
//“自己”类
// MySelf has a Friend;
class MySelf{
// 你这个对象,应该有一个朋友对象的电话号码。
// 电话号码就是一个对象的内存地址。联系你朋友的时候,打电话。
// f是一个引用。f默认值是null,是null表示,你没有朋友。
Friend f;
public MySelf(){
}
//通过构造方法能不能给你一个朋友对象。
public MySelf(Friend f){
this.f = f;
}
public static void main(String[] args){
// 创建朋友对象
Friend f = new Friend(); //朋友对象有了
// 创建对象的同时交朋友。
MySelf m2 = new MySelf(f);
// 创建自己对象
// 目前还没有交朋友。
MySelf m = new MySelf(); //自己对象
// 交朋友
m.f = f; // 把朋友的地址给了你。
}
}
// “朋友”类
class Friend{
}
*/
public class Test{
public static void main(String[] args){
// 创建厨师对象
//FoodMenu cooker1 = new ChinaCooker();
FoodMenu cooker1 = new AmericCooker();
// 创建顾客对象
Customer customer = new Customer(cooker1);
// 顾客点菜
customer.order();
}
}
1.4 包和import
包其实就是目录,特别是项目比较大,java 文件特别多的情况下,我们应该分目录管理,在 java
java.lang,此包Java语言标准包,使用此包中的内容无需import引入
java.sql,提供了JDBC接口类
java.util,提供了常用工具类
java.io,提供了各种输入输出流
1.4.1 包机制概述
编译:
javac -d . HelloWorld.java
解释一下:
javac 负责编译的命令
-d 带包编译
. 代表编译之后生成的东西放到当前目录下(点代表当前目录)
HelloWorld.java 被编译的java文件名。
运行:
java com.bjpowernode.javase.chapter17.HelloWorld
/*
关于java语言中的package和import机制:
1、为什么要使用package?
package是java中包机制。包机制的作用是为了方便程序的管理。
不同功能的类分别存放在不同的包下。(按照功能划分的,不同的
软件包具有不同的功能。)
2、package怎么用?
package是一个关键字,后面加包名。例如:
package com.bjpowernode.javase.chapter17;
注意:package语句只允许出现在java源代码的第一行。
3、包名有没有命名规范?有
一般都采用公司域名倒序的方式(因为公司域名具有全球唯一性。)
包名命名规范:
公司域名倒序 + 项目名 + 模块名 + 功能名
4、对于带有package的java程序怎么编译?怎么运行?
采用之前的编译和运行不行了。
类名不再是:HelloWorld了。
类名是:com.bjpowernode.javase.chapter17.HelloWorld
编译:
javac -d . HelloWorld.java
解释一下:
javac 负责编译的命令
-d 带包编译
. 代表编译之后生成的东西放到当前目录下(点代表当前目录)
HelloWorld.java 被编译的java文件名。
运行:
java com.bjpowernode.javase.chapter17.HelloWorld
5、关于import的使用。
import什么时候使用?
A类中使用B类。
A和B类都在同一个包下。不需要import。
A和B类不在同一个包下。需要使用import。
java.lang.*;这个包下的类不需要使用import导入。
import怎么用?
import语句只能出现在package语句之下,class声明语句之上。
import语句还可以采用星号的方式。
*/
package com.bjpowernode.javase.chapter17;
public class HelloWorld{
public static void main(String[] args){
System.out.println("Hello World!");
}
}
1.4.2 使用import机制
5、关于import的使用。
import什么时候使用?
A类中使用B类。
A和B类都在同一个包下。不需要import。
A和B类不在同一个包下。需要使用import。
java.lang.*;这个包下的类不需要使用import导入。
import怎么用?
import语句只能出现在package语句之下,class声明语句之上。
import语句还可以采用星号的方式。
package com.bjpowernode.javase.chapter17;
public class Test01{
public static void main(String[] args){
// 创建HelloWorld对象
// HelloWorld的完整类名:com.bjpowernode.javase.chapter17.HelloWorld
com.bjpowernode.javase.chapter17.HelloWorld hw = new com.bjpowernode.javase.chapter17.HelloWorld();
System.out.println(hw); //com.bjpowernode.javase.chapter17.HelloWorld@28a418fc
// 包名可以省略吗?
// 思考:这里的包名之所以可以省略,是因为HelloWorld和Test01在同一个package下。
HelloWorld hw2 = new HelloWorld();
System.out.println(hw2); //com.bjpowernode.javase.chapter17.HelloWorld@5305068a
}
}
package com;
// 将需要的类导入。
//import com.bjpowernode.javase.chapter17.HelloWorld;
import com.bjpowernode.javase.chapter17.*;
public class Test02{
public static void main(String[] args){
/*
Test02在com包下。
HelloWorld在com.bjpowernode.javase.chapter17下。
不在同一个package下,包名可以省略吗?
不能省略。
*/
//错误: 找不到符号
/*
HelloWorld hw = new HelloWorld();
System.out.println(hw);
*/
/*
com.bjpowernode.javase.chapter17.HelloWorld hw = new com.bjpowernode.javase.chapter17.HelloWorld();
System.out.println(hw);
com.bjpowernode.javase.chapter17.HelloWorld hw2 = new com.bjpowernode.javase.chapter17.HelloWorld();
System.out.println(hw2);
com.bjpowernode.javase.chapter17.HelloWorld hw3 = new com.bjpowernode.javase.chapter17.HelloWorld();
System.out.println(hw3);
*/
HelloWorld hw1 = new HelloWorld();
System.out.println(hw1);
HelloWorld hw2 = new HelloWorld();
System.out.println(hw2);
}
}
1.4.3 解释Scanner
package com.bjpowernode.javase.chapter17;
//import java.util.Scanner;
import java.util.*;
public class Test03{
public static void main(String[] args){
// 为什么要这样写?
// Test03类和Scanner类不在同一个包下。
// java.util就是Scanner类的包名。
//java.util.Scanner s = new java.util.Scanner(System.in);
Scanner s = new Scanner(System.in);
String str = s.next();
System.out.println("您输入的字符串是--->" + str);
java.lang.String name = "zhangsan";
System.out.println("名字是:" + name);
String username = "lisi";
System.out.println("用户名是:" + username);
}
}
1.5 访问控制权限
java访问级别修饰符主要包括:privateprotected和public,可以限定其他类对该类、属性和方法的使用权限,
注意以上对类的修饰只有:public和default,内部类除外
范围从大到小排序:public > protected > 默认 > private
package com.bjpowernode;
public class User{
//给一些属性
// 私有的
private int id;
// 受保护的
protected int age;
// 公开的
public int weight;
// 默认的
String name;
// 方法
public void m1(){}
private void m2(){}
void m3(){}
protected void m4(){}
// 静态方法也可以。
public static void x(){}
private static void y(){}
static void z(){}
protected static void k(){}
}
//错误: 此处不允许使用修饰符private
/*
private class MyClass1{
}
*/
//错误: 此处不允许使用修饰符protected
/*
protected class MyClass1{
}
*/
class MyClass1{
}
package com.bjpowernode;
public class Test01{
public static void main(String[] args){
User user = new User();
// private修饰的元素只能在本类中用。
//System.out.println(user.id);
System.out.println(user.age);
System.out.println(user.weight);
System.out.println(user.name);
}
}
package com.bjpowernode2; // 包变化了。
//import com.bjpowernode.*;
import com.bjpowernode.User;
public class Test02{
public static void main(String[] args){
User user = new User();
// 错误:protected在这里不行。
//System.out.println(user.age);
// 可以:公开的,在哪都行。
System.out.println(user.weight);
// 错误:“默认”在这里也不行。
//System.out.println(user.name);
}
}
package com.bjpowernode3; // 包变化了。
// 导入
import com.bjpowernode.User;
// User在com.bjpowernode包下。
// Vip在com.bjpowernode3包下。
// User和Vip不在同一个包下。
// 但是Vip是User的子类。
public class Vip extends User{
//实例方法
public void shopping(){
// this表示当前对象
// protected可以
System.out.println(this.age);
// 错误:默认 不行。
//System.out.println(this.name);
}
}
1.6 Object类
1.6.1 Object类的toString方法
/*
关于Object类中的toString()方法
1、源代码长什么样?
public String toString() {
return this.getClass().getName() + "@" + Integer.toHexString(hashCode());
}
源代码上toString()方法的默认实现是:
类名@对象的内存地址转换为十六进制的形式
2、SUN公司设计toString()方法的目的是什么?
toString()方法的作用是什么?
toString()方法的设计目的是:通过调用这个方法可以将一个“java对象”转换成“字符串表示形式”
3、其实SUN公司开发java语言的时候,建议所有的子类都去重写toString()方法。
toString()方法应该是一个简洁的、详实的、易阅读的.
*/
public class Test01{
public static void main(String[] args){
MyTime t1 = new MyTime(1970, 1, 1);
// 一个日期对象转换成字符串形式的话,我可能还是希望能看到具体的日期信息。
String s1 = t1.toString();
//MyTime类重写toString()方法之前
//System.out.println(s1); // MyTime@28a418fc
//MyTime类重写toString()方法之后
System.out.println(s1); // 1970年1月1日
//System.out.println(t1.toString()); //1970年1月1日
// 注意:输出引用的时候,会自动调用该引用的toString()方法。
System.out.println(t1);
}
}
class MyTime{
int year;
int month;
int day;
public MyTime(){
}
public MyTime(int year, int month, int day){
this.year = year;
this.month = month;
this.day = day;
}
// 重写toString()方法
// 这个toString()方法怎么重写呢?
// 越简洁越好,可读性越强越好。
// 向简洁的、详实的、易阅读的方向发展
public String toString(){
//return this.year + "年" + this.month + "月" + this.day + "日";
return this.year + "/" + this.month + "/" + this.day;
}
}
1.6.2 Object类的equals方法
/*
关于Object类中的equals方法
1、equals方法的源代码
public boolean equals(Object obj) {
return (this == obj);
}
以上这个方法是Object类的默认实现。
2、SUN公司设计equals方法的目的是什么?
以后编程的过程当中,都要通过equals方法来判断两个对象是否相等。
equals方法是判断两个对象是否相等的。
3、我们需要研究一下Object类给的这个默认的equals方法够不够用!!!!
在Object类中的equals方法当中,默认采用的是“==”判断两个java对象
是否相等。而“==”判断的是两个java对象的内存地址,我们应该判断
两个java对象的内容是否相等。所以老祖宗的equals方法不够用,
需要子类重写equals。
4、判断两个java对象是否相等,不能使用“==”,因为“==”比较的是两个
对象的内存地址。
*/
public class Test02{
public static void main(String[] args){
// 判断两个基本数据类型的数据是否相等直接使用“==”就行。
int a = 100;
int b = 100;
// 这个“==”是判断a中保存的100和b中保存的100是否相等。
System.out.println(a == b); //true(相等) false(不相等)
// 判断两个java对象是否相等,我们怎么办?能直接使用“==”吗?
// 创建一个日期对象是:2008年8月8日。
MyTime t1 = new MyTime(2008, 8, 8); //MyTime t1 = 0x1234;
// 创建了一个新的日期对象,但表示的日期也是:2008年8月8日。
MyTime t2 = new MyTime(2008, 8, 8); //MyTime t2 = 0x3698;
//测试一下,比较两个对象是否相等,能不能使用“==”???
// 这里的“==”判断的是:t1中保存的对象内存地址和t2中保存的对象内存地址是否相等。
System.out.println(t1 == t2); // false
// 重写Object equals方法之前(比较的是对象内存地址)
/*
boolean flag = t1.equals(t2);
System.out.println(flag); //false
*/
// 重写Object equals方法之后(比较的是内容。)
boolean flag = t1.equals(t2);
System.out.println(flag); //true
// 再创建一个新的日期
MyTime t3 = new MyTime(2008, 8, 9);
// 两个日期不相等,就是false。
System.out.println(t1.equals(t3)); // false
// 我们这个程序有bug吗?可以运行,但是效率怎么样?低(怎么改造。)
MyTime t4 = null;
System.out.println(t1.equals(t4)); //false
}
}
class MyTime { //extends Object{
int year;
int month;
int day;
public MyTime(){
}
public MyTime(int year, int month, int day){
this.year = year;
this.month = month;
this.day = day;
}
// 默认的equals方法
/*
public boolean equals(Object obj) {
return (this == obj);
}
*/
/*
// 重写Object类的equals方法
// 怎么重写?复制粘贴。相同的返回值类型、相同的方法名、相同的形式参数列表。
// equals到底应该怎么重写?你自己定,你认为两个对象什么相等的时候表示相等,你就怎么重写。
public boolean equals(Object obj) {
// 当年相同,月相同,并且日也相同的时候,表示两个日期相同。两个对象相等。
// 获取第一个日期的年月日
int year1 = this.year;
int month1 = this.month;
int day1 = this.day;
// 获取第二个日期的年月日
//int year2 = obj.year;
//int month2 = obj.month;
//int day2 = obj.day;
if(obj instanceof MyTime){
MyTime t = (MyTime)obj;
int year2 = t.year;
int month2 = t.month;
int day2 = t.day;
if(year1 == year2 && month1 == month2 && day1 == day2){
return true;
}
}
// 程序能够执行到此处表示日期不相等。
return false;
}
*/
/*
// 改良equals方法
public boolean equals(Object obj) {
// 如果obj是空,直接返回false
if(obj == null){
return false;
}
// 如果obj不是一个MyTime,没必要比较了 ,直接返回false
if(!(obj instanceof MyTime)){
return false;
}
// 如果this和obj保存的内存地址相同,没必要比较了,直接返回true。
// 内存地址相同的时候指向的堆内存的对象肯定是同一个。
if(this == obj){
return true;
}
// 程序能够执行到此处说明什么?
// 说明obj不是null,obj是MyTime类型。
MyTime t = (MyTime)obj;
if(this.year == t.year && this.month == t.month && this.day == t.day){
return true;
}
// 程序能到这里返回false
return false;
}
*/
//再次改良。
/*
public boolean equals(Object obj) {
// 如果obj是空,直接返回false
if(obj == null){
return false;
}
// 如果obj不是一个MyTime,没必要比较了 ,直接返回false
if(!(obj instanceof MyTime)){
return false;
}
// 如果this和obj保存的内存地址相同,没必要比较了,直接返回true。
// 内存地址相同的时候指向的堆内存的对象肯定是同一个。
if(this == obj){
return true;
}
// 程序能够执行到此处说明什么?
// 说明obj不是null,obj是MyTime类型。
MyTime t = (MyTime)obj;
return this.year == t.year && this.month == t.month && this.day == t.day ;
}
*/
public boolean equals(Object obj) {
if(obj == null || !(obj instanceof MyTime)){
return false;
}
if(this == obj){
return true;
}
MyTime t = (MyTime)obj;
return this.year == t.year && this.month == t.month && this.day == t.day ;
}
}
/*
class Person{
private String idCard;
}
*/
String类重写了toString和equals
/*
java语言当中的字符串String有没有重写toString方法,有没有重写equals方法。
总结:
1、String类已经重写了equals方法,比较两个字符串不能使用==,必须使用equals。
equals是通用的。
2、String类已经重写了toString方法。
大结论:
java中什么类型的数据可以使用“==”判断
java中基本数据类型比较是否相等,使用==
java中什么类型的数据需要使用equals判断
java中所有的引用数据类型统一使用equals方法来判断是否相等。
这是规矩。
*/
public class Test03{
public static void main(String[] args){
// 大部分情况下,采用这样的方式创建字符串对象
String s1 = "hello";
String s2 = "abc";
// 实际上String也是一个类。不属于基本数据类型。
// 既然String是一个类,那么一定存在构造方法。
String s3 = new String("Test1");
String s4 = new String("Test1");
// new两次,两个对象内存地址,s3保存的内存地址和s4保存的内存地址不同。
// == 判断的是内存地址。不是内容。
System.out.println(s3 == s4); // false
// 比较两个字符串能不能使用双等号?
// 不能,必须调用equals方法。
// String类已经重写equals方法了。
System.out.println(s3.equals(s4)); // true
// String类有没有重写toString方法呢?
String x = new String("动力节点");
// 如果String没有重写toString()方法,输出结果:java.lang.String@十六进制的地址
// 经过测试:String类已经重写了toString()方法。
System.out.println(x.toString()); //动力节点
System.out.println(x); //动力节点
}
}
// String对象比较的时候必须使用equals方法。
public class Test04{
public static void main(String[] args){
/*
Student s1 = new Student(111, "北京大兴亦庄二小");
Student s2 = new Student(111, "北京大兴亦庄二小");
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true
*/
Student s1 = new Student(111, new String("北京大兴亦庄二小"));
Student s2 = new Student(111, new String("北京大兴亦庄二小"));
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true
}
}
class Student{
// 学号
int no; //基本数据类型,比较时使用:==
// 所在学校
String school; //引用数据类型,比较时使用:equals方法。
public Student(){}
public Student(int no,String school){
this.no = no;
this.school = school;
}
// 重写toString方法
public String toString(){
return "学号" + no + ",所在学校名称" + school;
}
// 重写equals方法
// 需求:当一个学生的学号相等,并且学校相同时,表示同一个学生。
// 思考:这个equals该怎么重写呢?
// equals方法的编写模式都是固定的。架子差不多。
public boolean equals(Object obj){
if(obj == null || !(obj instanceof Student)) return false;
if(this == obj) return true;
Student s = (Student)obj;
return this.no == s.no && this.school.equals(s.school);
//字符串用双等号比较可以吗?
// 不可以
//return this.no == s.no && this.school == s.school;
}
}
equals方法深层次理解
// equals方法重写的时候要彻底。
public class Test05{
public static void main(String[] args){
// 多态(自动类型转换。)
Object o1 = new String("hello world!");
Object o2 = new User();
Object o3 = new Address();
User u1 = new User("zhangsan", new Address("北京","大兴区","11111"));
User u2 = new User("zhangsan", new Address("北京","大兴区","11111"));
System.out.println(u1.equals(u2)); // true
User u3 = new User("zhangsan", new Address("北京","朝阳区","11112"));
System.out.println(u1.equals(u3)); // false
}
}
class User{
// 用户名
String name;
// 用户的住址
Address addr;
public User(){
}
public User(String name, Address addr){
this.name = name;
this.addr = addr;
}
// 重写equals方法
// 重写规则:当一个用户的用户名和家庭住址都相同,表示同一个用户。
// 这个equals判断的是User对象和User对象是否相等。
public boolean equals(Object obj){
// 用户名和用户名相同,住址和住址相同的时候,认定是同一个用户。
if(obj == null || !(obj instanceof User)) return false;
if(this == obj) return true;
User u = (User)obj;
if(this.name.equals(u.name) && this.addr.equals(u.addr)){
return true;
}
return false;
}
}
class Address{
String city;
String street;
String zipcode;
public Address(){
}
public Address(String city,String street,String zipcode){
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
// 注意:这里并没有重写equals方法。
// 这里的equals方法判断的是:Address对象和Address对象是否相等。
public boolean equals(Object obj){
if(obj == null || !(obj instanceof Address)) return false;
if(this == obj) return true;
// 怎么算是家庭住址相同呢?
// 城市相同,街道相同,邮编相同,表示相同。
Address a = (Address)obj;
if(this.city.equals(a.city)
&& this.street.equals(a.street)
&& this.zipcode.equals(a.zipcode)){
return true;
}
return false;
}
}
1.6.3 Object类的finalize方法
这个方法不需要程序员手动调用,JVM的垃圾回收器负责调用这个方法。不像equals toString,equals和toString()方法是需要你写代码调用的。finalize()只需要重写,重写完将来自动会有程序来调用。
当一个java对象即将被垃圾回收器回收的时候,垃圾回收器负责调用finalize()方法。
/*
关于Object类中的finalize()方法。(非重点 非重点 非重点 了解即可。)
1、在Object类中的源代码:
protected void finalize() throws Throwable { }
GC:负责调用finalize()方法。
2、finalize()方法只有一个方法体,里面没有代码,而且这个方法是protected修饰的。
3、这个方法不需要程序员手动调用,JVM的垃圾回收器负责调用这个方法。
不像equals toString,equals和toString()方法是需要你写代码调用的。
finalize()只需要重写,重写完将来自动会有程序来调用。
4、finalize()方法的执行时机:
当一个java对象即将被垃圾回收器回收的时候,垃圾回收器负责调用
finalize()方法。
5、finalize()方法实际上是SUN公司为java程序员准备的一个时机,垃圾销毁时机。
如果希望在对象销毁时机执行一段代码的话,这段代码要写到finalize()方法当中。
6、静态代码块的作用是什么?
static {
....
}
静态代码块在类加载时刻执行,并且只执行一次。
这是一个SUN准备的类加载时机。
finalize()方法同样也是SUN为程序员准备的一个时机。
这个时机是垃圾回收时机。
7、提示:
java中的垃圾回收器不是轻易启动的,
垃圾太少,或者时间没到,种种条件下,
有可能启动,也有可能不启动。
*/
public class Test06{
public static void main(String[] args){
/*
// 创建对象
Person p = new Person();
// 怎么把Person对象变成垃圾?
p = null;
*/
// 多造点垃圾
/*
for(int i = 0; i < 100000000; i++){
Person p = new Person();
p = null;
}
*/
for(int i = 0; i < 1000; i++){
Person p = new Person();
p = null;
// 有一段代码可以建议垃圾回收器启动。
if(i % 2 == 0){
System.gc(); // 建议启动垃圾回收器。(只是建议,可能不启动,也可能启动。启动的概率高了一些。)
}
}
}
}
// 项目开发中有这样的业务需求:所有对象在JVM中被释放的时候,请记录一下释放时间!!!
// 记录对象被释放的时间点,这个负责记录的代码写到哪里?
// 写到finalize()方法中。
class Person{
// 重写finalize()方法
// Person类型的对象被垃圾回收器回收的时候,垃圾回收器负责调用:p.finalize();
protected void finalize() throws Throwable {
// this代表当前对象
System.out.println(this + "即将被销毁!");
}
}
1.6.4 Object类的hashCode方法
hashCode()方法返回的是哈希码:
实际上就是一个java对象的内存地址,经过哈希算法,得出的一个值。
所以hashCode()方法的执行结果可以等同看做一个java对象的内存地址。
/*
hashCode方法:
在Object中的hashCode方法是怎样的?
public native int hashCode();
这个方法不是抽象方法,带有native关键字,底层调用C++程序。
hashCode()方法返回的是哈希码:
实际上就是一个java对象的内存地址,经过哈希算法,得出的一个值。
所以hashCode()方法的执行结果可以等同看做一个java对象的内存地址。
*/
public class Test07{
public static void main(String[] args){
Object o = new Object();
int hashCodeValue = o.hashCode();
// 对象内存地址经过哈希算法转换的一个数字。可以等同看做内存地址。
System.out.println(hashCodeValue); //798154996
MyClass mc = new MyClass();
int hashCodeValue2 = mc.hashCode();
System.out.println(hashCodeValue2); //1392838282
MyClass mc2 = new MyClass();
System.out.println(mc2.hashCode()); // 523429237
}
}
class MyClass
{
}
1.7 内部类
内部类:
在一个类的内部定义的类,称为内部类
内部类主要分类:
实例内部类
局部内部类
静态内部类
实例内部类:
创建实例内部类,外部类的实例必须已经创建
实例内部类会持有外部类的引用
实例内部不能定义static成员,只能定义实例成员
静态内部类:
静态内部类不会持有外部的类的引用,创建时可以不用创建外部类
静态内部类可以访问外部的静态变量,如果访问外部类的成员变量必须通过外部类的实例访问
局部内部类:
局部内部类是在方法中定义的,它只能在当前方法中使用。和局部变量的作用一样
局部内部类和实例内部类一致,不能包含静态成员
匿名内部类:
是一种特殊的内部类,该类没有名字
匿名内部类是局部内部类的一种。因为这个类没有名字而得名,叫做匿名内部类。
匿名内部类:new 接口,然后{}里写对接口方法的实现
/*
匿名内部类:
1、什么是内部类?
内部类:在类的内部又定义了一个新的类。被称为内部类。
2、内部类的分类:
静态内部类:类似于静态变量
实例内部类:类似于实例变量
局部内部类:类似于局部变量
3、使用内部类编写的代码,可读性很差。能不用尽量不用。
4、匿名内部类是局部内部类的一种。
因为这个类没有名字而得名,叫做匿名内部类。
5、学习匿名内部类主要是让大家以后在阅读别人代码的时候,能够理解。
并不代表以后都要这样写。因为匿名内部类有两个缺点:
缺点1:太复杂,太乱,可读性差。
缺点2:类没有名字,以后想重复使用,不能用。
6、不理解算了,你只要记住这种写法就行。
*/
class Test01{
// 静态变量
static String country;
// 该类在类的内部,所以称为内部类
// 由于前面有static,所以称为“静态内部类”
static class Inner1{
}
// 实例变量
int age;
// 该类在类的内部,所以称为内部类
// 没有static叫做实例内部类。
class Inner2{
}
// 方法
public void doSome(){
// 局部变量
int i = 100;
// 该类在类的内部,所以称为内部类
// 局部内部类。
class Inner3{
}
}
public void doOther(){
// doSome()方法中的局部内部类Inner3,在doOther()中不能用。
}
// main方法,入口
public static void main(String[] args){
// 调用MyMath中的mySum方法。
MyMath mm = new MyMath();
/*
Compute c = new ComputeImpl();
mm.mySum(c, 100, 200);
*/
//合并(这样写代码,表示这个类名是有的。类名是:ComputeImpl)
//mm.mySum(new ComputeImpl(), 100, 200);
// 使用匿名内部类,表示这个ComputeImpl,则这个类没名字了。
// 这里表面看上去好像是接口可以直接new了,实际上并不是接口可以new了。
// 后面的{} 代表了对接口的实现。
// 不建议使用匿名内部类,为什么?
// 因为一个类没有名字,没有办法重复使用。另外代码太乱,可读性太差。
mm.mySum(new Compute(){
public int sum(int a, int b){
return a + b;
}
}, 200, 300);
}
}
// 负责计算的接口
interface Compute{
// 抽象方法
int sum(int a, int b);
}
// 你自动会在这里编写一个Compute接口的实现类
/*
class ComputeImpl implements Compute{
// 对方法的实现
public int sum(int a, int b){
return a + b;
}
}
*/
// 数学类
class MyMath{
// 数学求和方法
public void mySum(Compute c, int x, int y){
int retValue = c.sum(x, y);
System.out.println(x + "+" + y + "=" + retValue);
}
}
1.8 day20课堂笔记(final、抽象类、接口)
1、final关键字
1.1、final修饰的类无法继承。
1.2、final修饰的方法无法覆盖。
1.3、final修饰的变量只能赋一次值。
1.4、final修饰的引用一旦指向某个对象,则不能再重新指向其它对象,但该引用指向的对象内部的数据是可以修改的。
1.5、final修饰的实例变量必须手动初始化,不能采用系统默认值。
1.6、final修饰的实例变量一般和static联合使用,称为常量。
public static final double PI = 3.1415926;
2、抽象类和接口以及抽象类和接口的区别。
2.1、抽象类
第一:抽象类怎么定义?在class前添加abstract关键字就行了。
第二:抽象类是无法实例化的,无法创建对象的,所以抽象类是用来被子类继承的。
第三:final和abstract不能联合使用,这两个关键字是对立的。
第四:抽象类的子类可以是抽象类。也可以是非抽象类。
第五:抽象类虽然无法实例化,但是抽象类有构造方法,这个构造方法是供子类使用的。
第六:抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中。
第七:抽象方法怎么定义?
public abstract void doSome();
第八(*****五颗星):一个非抽象的类,继承抽象类,必须将抽象类中的抽象方法进行覆盖/重写/实现。
到目前为止,只是学习了抽象类的基础语法,一个类到底声明为抽象还是非抽象,这个以后慢慢来吧。写代码多的时候,自然就理解了。
面试题(判断题):java语言中凡是没有方法体的方法都是抽象方法。
不对,错误的。
Object类中就有很多方法都没有方法体,都是以“;”结尾的,但他们都不是抽象方法,例如:
public native int hashCode();
这个方法底层调用了C++写的动态链接库程序。
前面修饰符列表中没有:abstract。有一个native。表示调用JVM本地程序。
2.2、接口的基础语法。
1、接口是一种“引用数据类型”。
2、接口是完全抽象的。
3、接口怎么定义:[修饰符列表] interface 接口名{}
4、接口支持多继承。
5、接口中只有常量+抽象方法。
6、接口中所有的元素都是public修饰的
7、接口中抽象方法的public abstract可以省略。
8、接口中常量的public static final可以省略。
9、接口中方法不能有方法体。
2.3、接口在开发中的作用。
1.9 day21课堂笔记(接口,抽象类和接口的区别)
1、抽象类和接口以及抽象类和接口的区别。
1.1、抽象类的基础语法(见昨天笔记)
1.2、接口的基础语法
1、接口是一种“引用数据类型”。
2、接口是完全抽象的。
3、接口怎么定义:[修饰符列表] interface 接口名{}
4、接口支持多继承。
5、接口中只有常量+抽象方法。
6、接口中所有的元素都是public修饰的
7、接口中抽象方法的public abstract可以省略。
8、接口中常量的public static final可以省略。
9、接口中方法不能有方法体。
10、一个非抽象的类,实现接口的时候,必须将接口中所有方法加以实现。
11、一个类可以实现多个接口。
12、extends和implements可以共存,extends在前,implements在后。
13、使用接口,写代码的时候,可以使用多态(父类型引用指向子类型对象)。
课下代码就不要翻阅讲师的了,直接根据这个结论进行验证。推翻它。尝试推翻它。
1.3、接口在开发中的作用
注意:接口在开发中的作用,类似于多态在开发中的作用。
多态:面向抽象编程,不要面向具体编程。降低程序的耦合度。提高程序的扩展力。
/*
public class Master{
public void feed(Dog d){}
public void feed(Cat c){}
//假设又要养其它的宠物,那么这个时候需要再加1个方法。(需要修改代码了)
//这样扩展力太差了,违背了OCP原则(对扩展开放,对修改关闭。)
}
*/
public class Master{
public void feed(Animal a){
// 面向Animal父类编程,父类是比子类更抽象的。
//所以我们叫做面向抽象编程,不要面向具体编程。
//这样程序的扩展力就强。
}
}
接口在开发中的作用?
接口是不是完全抽象的?是。
而我们以后正好要求,面向抽象编程。
面向抽象编程这句话以后可以修改为:面向接口编程。(面向接口编程就是面向抽象编程)
有了接口就有了可插拔。可插拔表示扩展力很强。不是焊接死的。
主板和内存条之间有插槽,这个插槽就是接口,内存条坏了,可以重新买一个换下来。这叫做高扩展性。(低耦合度。)
接口在现实世界中是不是到处都是呢?
螺栓和螺母之间有接口
灯泡和灯口之间有接口
笔记本电脑和键盘之间有接口(usb接口,usb接口是不是某个计算机协会制定的协议/规范。)
接口有什么用?扩展性好。可插拔。
接口是一个抽象的概念。
分析:
中午去饭馆吃饭,这个过程中有接口吗?
接口是抽象的。
菜单是一个接口。(菜单上有一个抽象的照片:西红柿炒鸡蛋)
谁面向接口调用。(顾客面向菜单点菜,调用接口。)
谁负责实现这个接口。(后台的厨师负责把西红柿鸡蛋做好!是接口的实现者。)
这个接口有什么用呢?
这个饭馆的“菜单”,让“顾客”和“后厨”解耦合了。
顾客不用找后厨,后厨不用找顾客。他们之间完全依靠这个抽象的菜单沟通。
总结一句话:三个字“解耦合”
面向接口编程,可以降低程序的耦合度,提高程序的扩展力。符合OCP开发原则。
接口的使用离不开多态机制。(接口+多态才可以达到降低耦合度。)
接口可以解耦合,解开的是谁和谁的耦合!!!
任何一个接口都有调用者和实现者。
接口可以将调用者和实现者解耦合。
调用者面向接口调用。
实现者面向接口编写实现。
以后进行大项目的开发,一般都是将项目分离成一个模块一个模块的,
模块和模块之间采用接口衔接。降低耦合度。
1.4、类型和类型之间的关系:
is a(继承)、has a(关联)、like a(实现)
is a:
Cat is a Animal(猫是一个动物)
凡是能够满足is a的表示“继承关系”
A extends B
has a:
I has a Pen(我有一支笔)
凡是能够满足has a关系的表示“关联关系”
关联关系通常以“属性”的形式存在。
A{
B b;
}
like a:
Cooker like a FoodMenu(厨师像一个菜单一样)
凡是能够满足like a关系的表示“实现关系”
实现关系通常是:类实现接口。
A implements B
1.5、抽象类和接口有什么区别?
在这里我们只说一下抽象类和接口在语法上的区别。
至于以后抽象类和接口应该怎么进行选择,通过后面的项目去体会/学习。
抽象类是半抽象的。
接口是完全抽象的。
抽象类中有构造方法。
接口中没有构造方法。
接口和接口之间支持多继承。
类和类之间只能单继承。
一个类可以同时实现多个接口。
一个抽象类只能继承一个类(单继承)。
接口中只允许出现常量和抽象方法。
这里先透露一个信息:
以后接口使用的比抽象类多。一般抽象类使用的还是少。
接口一般都是对“行为”的抽象。
2、package和import
2.1、package
第一:package出现在java源文件第一行。
第二:带有包名怎么编译?javac -d . xxx.java
第三:怎么运行?java 完整类名
补充:以后说类名的时候,如果带着包名描述,表示完整类名。如果没有带包,描述的话,表示简类名。
java.util.Scanner 完整类名。
Scanner 简类名
2.2、import
import什么时候不需要?
java.lang不需要。
同包下不需要。
其它一律都需要。
怎么用?
import 完整类名;
import 包名.*;
import java.util.Scanner; // 完整类名。
// 同学的疑问:这样是不是效率比较低。
// 这个效率不低,因为编译器在编译的时候,会自动把*变成具体的类名。
import java.util.*;
// 想省懒劲你不能太省了。
import java.*; 这是不允许的,因为在java语言中规定,这里的*只代表某些类的名字。
1.10 day22课堂笔记(访问控制权限、Object类、内部类)
1、访问控制权限
1.1、访问控制权限都有哪些?
4个。
private 私有
public 公开
protected 受保护
默认
1.2、以上的4个访问控制权限:控制的范围是什么?
private 表示私有的,只能在本类中访问
public 表示公开的,在任何位置都可以访问
“默认”表示只能在本类,以及同包下访问。
protected表示只能在本类、同包、子类中访问。
访问控制修饰符 本类 同包 子类 任意位置
---------------------------------------------------------------------------------------------------
public 可以 可以 可以 可以
protected 可以 可以 可以 不行
默认 可以 可以 不行 不行
private 可以 不行 不行 不行
这个不要死记硬背,自己下去之后编写代码自己测试。
范围从大到小排序:public > protected > 默认 > private
1.3、访问控制权限修饰符可以修饰什么?
属性(4个都能用)
方法(4个都能用)
类(public和默认能用,其它不行。)
接口(public和默认能用,其它不行。)
.....
2、JDK类库的根类:Object
2.1、这个老祖宗类中的方法我们需要先研究一下,因为这些方法都是所有子类通用的。
任何一个类默认继承Object。就算没有直接继承,最终也会间接继承。
2.2、Object类当中有哪些常用的方法?
我们去哪里找这些方法呢?
第一种方法:去源代码当中。(但是这种方式比较麻烦,源代码也比较难)
第二种方法:去查阅java的类库的帮助文档。
什么是API?
应用程序编程接口。(Application Program Interface)
整个JDK的类库就是一个javase的API。
每一个API都会配置一套API帮助文档。
SUN公司提前写好的这套类库就是API。(一般每一份API都对应一份API帮助文档。)
目前为止我们只需要知道这几个方法即可:
protected Object clone() // 负责对象克隆的。
int hashCode() // 获取对象哈希值的一个方法。
boolean equals(Object obj) // 判断两个对象是否相等
String toString() // 将对象转换成字符串形式
protected void finalize() // 垃圾回收器负责调用的方法
2.3、toString()方法
以后所有类的toString()方法是需要重写的。
重写规则,越简单越明了就好。
System.out.println(引用); 这里会自动调用“引用”的toString()方法。
String类是SUN写的,toString方法已经重写了。
2.4、equals()方法
以后所有类的equals方法也需要重写,因为Object中的equals方法比较的是两个对象的内存地址,我们应该比较内容,所以需要重写。
重写规则:自己定,主要看是什么和什么相等时表示两个对象相等。
基本数据类型比较使用:==
对象和对象比较:调用equals方法
String类是SUN编写的,所以String类的equals方法重写了。
以后判断两个字符串是否相等,最好不要使用==,要调用字符串对象的equals方法。
注意:重写equals方法的时候要彻底。
2.5、finalize()方法。
这个方法是protected修饰的,在Object类中这个方法的源代码是?
protected void finalize() throws Throwable { }
3、匿名内部类
02 数组
数组是一种引用数据类型,在内存中存储示意图如下:
1.数组是一组数据的集合
2.数组作为一种引用类型
3.数组元素的类型可以是基本类型,也可以是引用类型,但同一个数组只能是同一种类型
4.数组作为对象,数组中的元素作为对象的属性,除此之外数组还包括一个成员属性length,length表示数组的长度
5.数组的长度在数组对象创建后就确定了,就无法再修改了
6.数组元素是有下标的,下标从0开始,也就是第一个元素的下标为0,依次类推最后一个元素的下标为n-1,我们可以通过数组的下标来访问数组的元素
2.1 一维数组概述
一维数组的声明格式有以下两种:
1.数组元素的类型[]变量名称
2.数组元素的类型变量名称[]数组元素的类型,可以是java中的任意类型,变量名称可以是任意合法的标识符,上面两种格式较常用的是第一种,例如:
int[] a;
Student[] stu
在一行中也可以声明多个数组,例如:
int[] a,b,c
2.1.1 一维数组的声明和使用
002-数组的内存结构
package com.bjpowernode.javase.array;
/*
Array
1、Java语言中的数组是一种引用数据类型。不属于基本数据类型。数组的父类是Object。
2、数组实际上是一个容器,可以同时容纳多个元素。(数组是一个数据的集合。)
数组:字面意思是“一组数据”
3、数组当中可以存储“基本数据类型”的数据,也可以存储“引用数据类型”的数据。
4、数组因为是引用类型,所以数组对象是堆内存当中。(数组是存储在堆当中的)
5、数组当中如果存储的是“java对象”的话,实际上存储的是对象的“引用(内存地址)”,数组中不能直接存储java对象。
6、数组一旦创建,在java中规定,长度不可变。(数组长度不可变)
7、数组的分类:一维数组、二维数组、三维数组、多维数组...(一维数组较多,二维数组偶尔使用!)
8、所有的数组对象都有length属性(java自带的),用来获取数组中元素的个数。
9、java中的数组要求数组中元素的类型统一。比如int类型数组只能存储int类型,Person类型数组只能存储Person类型。
例如:超市购物,购物袋中只能装苹果,不能同时装苹果和橘子。(数组中存储的元素类型统一)
10、数组在内存方面存储的时候,数组中的元素内存地址(存储的每一个元素都是有规则的挨着排列的)是连续的。内存地址连续。
这是数组存储元素的特点(特色)。数组实际上是一种简单的数据结构。
11、所有的数组都是拿“第一个小方框的内存地址”作为整个数组对象的内存地址。
(数组中首元素的内存地址作为整个数组对象的内存地址。)
12、数组中每一个元素都是有下标的,下标从0开始,以1递增。最后一个元素的下标是:length - 1
下标非常重要,因为我们对数组中元素进行“存取”的时候,都需要通过下标来进行。
13、数组这种数据结构的优点和缺点是什么?
优点:查询/查找/检索某个下标上的元素时效率极高。可以说是查询效率最高的一个数据结构。
为什么检索效率高?
第一:每一个元素的内存地址在空间存储上是连续的。
第二:每一个元素类型相同,所以占用空间大小一样。
第三:知道第一个元素内存地址,知道每一个元素占用空间的大小,又知道下标,所以
通过一个数学表达式就可以计算出某个下标上元素的内存地址。直接通过内存地址定位
元素,所以数组的检索效率是最高的。
数组中存储100个元素,或者存储100万个元素,在元素查询/检索方面,效率是相同的,
因为数组中元素查找的时候不会一个一个找,是通过数学表达式计算出来的。(算出一个
内存地址,直接定位的。)
缺点:
第一:由于为了保证数组中每个元素的内存地址连续,所以在数组上随机删除或者增加元素的时候,
效率较低,因为随机增删元素会涉及到后面元素统一向前或者向后位移的操作。
第二:数组不能存储大数据量,为什么?
因为很难在内存空间上找到一块特别大的连续的内存空间。
注意:对于数组中最后一个元素的增删,是没有效率影响的。
14、怎么声明/定义一个一维数组?
语法格式:
int[] array1;
double[] array2;
boolean[] array3;
String[] array4;
Object[] array5;
15、怎么初始化一个一维数组呢?
包括两种方式:静态初始化一维数组,动态初始化一维数组。
静态初始化语法格式:
int[] array = {100, 2100, 300, 55};
动态初始化语法格式:
int[] array = new int[5]; // 这里的5表示数组的元素个数。
// 初始化一个5个长度的int类型数组,每个元素默认值0
String[] names = new String[6]; // 初始化6个长度的String类型数组,每个元素默认值null。
*/
public class ArrayTest01 {
public static void main(String[] args) {
// 声明一个int类型的数组,使用静态初始化的方式
int[] a = {1, 100, 10, 20, 55, 689};
// 这是C++风格,不建议java中使用。
//int a[] = {1, 100, 10, 20, 55, 689};
// 所有的数组对象都有length属性
System.out.println("数组中元素的个数" + a.length);
// 数组中每一个元素都有下标
// 通过下标对数组中的元素进行存和取。
// 取(读)
System.out.println("第一个元素 = " + a[0]);
System.out.println("最后一个元素 = " + a[5]);
System.out.println("最后一个元素 = " + a[a.length - 1]);
// 存(改)
// 把第一个元素修改为111
a[0] = 111;
// 把最后一个元素修改为0
a[a.length - 1] = 0;
System.out.println("第一个元素 = " + a[0]);
System.out.println("最后一个元素 = " + a[5]);
// 一维数组怎么遍历呢?
for(int i = 0; i < a.length; i++){
System.out.println(a[i]); // i是从0到5,是下标
}
// 下标为6表示第7个元素,第7个元素没有,下标越界了。会出现什么异常呢?
//System.out.println(a[6]); //ArrayIndexOutOfBoundsException(比较著名的异常。)
// 从最后一个元素遍历到第1个元素
for (int i = a.length - 1; i >= 0; i--) {
System.out.println("颠倒顺序输出-->" + a[i]);
}
}
}
2.1.2 动态初始化一维数组
001-数组内存图
package com.bjpowernode.javase.array;
/*
关于每个类型的默认值还有印象吗?
数据类型 默认值
----------------------------
byte 0
short 0
int 0
long 0L
float 0.0F
double 0.0
boolean false
char \u0000
引用数据类型 null
什么时候采用静态初始化方式,什么时候使用动态初始化方式呢?
当你创建数组的时候,确定数组中存储哪些具体的元素时,采用静态初始化方式。
当你创建数组的时候,不确定将来数组中存储哪些数据,你可以采用动态初始化的方式,预先分配内存空间。
*/
public class ArrayTest02 {
public static void main(String[] args) {
// 声明/定义一个数组,采用动态初始化的方式创建
int[] a = new int[4]; // 创建长度为4的int数组,数组中每个元素的默认值是0
// 遍历数组
for (int i = 0; i < a.length; i++) {
System.out.println("数组中下标为" + i + "的元素是:" + a[i]);
}
// 后期赋值
a[0] = 1;
a[1] = 100;
a[2] = 111;
a[3] = 222; // 注意下标别越界。
for (int i = 0; i < a.length; i++) {
System.out.println("数组中下标为" + i + "的元素是:" + a[i]);
}
// 初始化一个Object类型的数组,采用动态初始化方式
Object[] objs = new Object[3]; // 3个长度,动态初始化,所以每个元素默认值是null
for (int i = 0; i < objs.length; i++) {
System.out.println(objs[i]);
}
System.out.println("===============================");
String[] strs = new String[3];
for (int i = 0; i < strs.length; i++) {
System.out.println(strs[i]);
}
// 采用静态初始化的方式
String[] strs2 = {"abc", "def", "xyz"};
for (int i = 0; i < strs2.length; i++) {
System.out.println(strs2[i]);
}
// 存储Object,采用静态初始化呢?
/*Object o1 = new Object();
Object o2 = new Object();
Object o3 = new Object();
Object[] objects = {o1, o2, o3};*/
Object[] objects = {new Object(), new Object(), new Object()};
for (int i = 0; i < objects.length; i++) {
/*Object o = objects[i];
System.out.println(o);*/
System.out.println(objects[i]);
}
}
}
2.1.3 方法的参数是数组
package com.bjpowernode.javase.array;
// 当一个方法上,参数的类型是一个数组的时候。
public class ArrayTest03 {
// main方法的编写方式,还可以采用C++的语法格式哦!
public static void main(String args[]) {
System.out.println("hello world!");
// java的风格。
int[] a1 = {1,23,3};
for (int i = 0; i < a1.length ; i++) {
System.out.println(a1[i]);
}
// C++的写法,不建议。
int a2[] = {3,4,2};
for (int i = 0; i < a2.length ; i++) {
System.out.println(a2[i]);
}
System.out.println("===================================");
// 调用方法时传一个数组
int[] x = {1,2,3,4};
printArray(x);
// 创建String数组
String[] stringArray = {"abc", "def", "hehe", "haha"};
printArray(stringArray);
String[] strArray = new String[10];
printArray(strArray); // 10个null
System.out.println("================================");
printArray(new String[3]);
System.out.println("***********************************");
printArray(new int[4]);
}
public static void printArray(int[] array){
for(int i = 0; i < array.length; i++){
System.out.println(array[i]);
}
}
public static void printArray(String[] args){
for(int i = 0; i < args.length; i++){
System.out.println("String数组中的元素:" + args[i]);
}
}
}
package com.bjpowernode.javase.array;
// 当一个方法的参数是一个数组的时候,我们还可以采用这种方式传。
public class ArrayTest04 {
public static void main(String[] args) {
// 静态初始化一维数组
int[] a = {1,2,3};
printArray(a);
System.out.println("============================");
// 没有这种语法。
//printArray({1,2,3});
// 如果直接传递一个静态数组的话,语法必须这样写。
printArray(new int[]{1,2,3});
// 动态初始化一维数组
int[] a2 = new int[4];
printArray(a2);
System.out.println("=============================");
printArray(new int[3]);
}
// 为什么要使用静态方法?方便呀,不需要new对象啊。
public static void printArray(int[] array){
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
}
2.1.4 main方法的String数组
main方法上面的String[] args数组主要是用来接收用户输入参数的。
package com.bjpowernode.javase.array;
/*
1、main方法上面的“String[] args”有什么用?
分析以下:谁负责调用main方法(JVM)
JVM调用main方法的时候,会自动传一个String数组过来。
*/
public class ArrayTest05 {
// 这个方法程序员负责写出来,JVM负责调用。JVM调用的时候一定会传一个String数组过来。
public static void main(String[] args) {
// JVM默认传递过来的这个数组对象的长度?默认0
// 通过测试得出:args不是null。
System.out.println("JVM给传递过来的String数组参数,它这个数组的长度是?" + args.length);
// 以下这一行代码表示的含义:数组对象创建了,但是数组中没有任何数据。
//String[] strs = new String[0];
//String[] strs = {}; // 静态初始化数组,里面没东西。
//printLength(strs);
// 这个数组什么时候里面会有值呢?
// 其实这个数组是留给用户的,用户可以在控制台上输入参数,这个参数自动会被转换为“String[] args”
// 例如这样运行程序:java ArrayTest05 abc def xyz
// 那么这个时候JVM会自动将“abc def xyz”通过空格的方式进行分离,分离完成之后,自动放到“String[] args”数组当中。
// 所以main方法上面的String[] args数组主要是用来接收用户输入参数的。
// 把abc def xyz 转换成字符串数组:{"abc","def","xyz"}
// 遍历数组
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
public static void printLength(String[] args){
System.out.println(args.length); // 0
}
}
package com.bjpowernode.javase.array;
/*
模拟一个系统,假设这个系统要使用,必须输入用户名和密码。
*/
public class ArrayTest06 {
// 用户名和密码输入到String[] args数组当中。
public static void main(String[] args) {
if(args.length != 2){
System.out.println("使用该系统时请输入程序参数,参数中包括用户名和密码信息,例如:zhangsan 123");
return;
}
// 程序执行到此处说明用户确实提供了用户名和密码。
// 接下来你应该判断用户名和密码是否正确。
// 取出用户名
String username = args[0];
// 取出密码
String password = args[1];
// 假设用户名是admin,密码是123的时候表示登录成功。其它一律失败。
// 判断两个字符串是否相等,需要使用equals方法。
//if(username.equals("admin") && password.equals("123")){
// 这样编写是不是可以避免空指针异常。
// 采用以下编码风格,及时username和password都是null,也不会出现空指针异常。(这是老程序员给的一条编程经验。)
if("admin".equals(username) && "123".equals(password)){
System.out.println("登录成功,欢迎[" + username + "]回来");
System.out.println("您可以继续使用该系统....");
}else{
System.out.println("验证失败,用户名不存在或者密码错误!");
}
}
}
2.1.5 数组中存储引用数据类型
package com.bjpowernode.javase.array;
/**
* 一维数组的深入,数组中存储的类型为:引用数据类型
* 对于数组来说,实际上只能存储java对象的“内存地址”。数组中存储的每个元素是“引用”。
*/
public class ArrayTest07 {
public static void main(String[] args) {
// a是一个数组
// a[0] 是数组中的一个元素。
// a[1] 是数组中的一个元素。
int[] a = {100, 200, 300};
System.out.println(a[1]);
//a[2] = 400;
int[] array = {1,2,3};
for (int i = 0; i < array.length; i++) {
/*int temp = array[i];
System.out.println(temp);*/
System.out.println(array[i]);
}
// 创建一个Animal类型的数组
Animal a1 = new Animal();
Animal a2 = new Animal();
Animal[] animals = {a1, a2};
// 对Animal数组进行遍历
for (int i = 0; i < animals.length; i++) {
/*Animal a = animals[i];
a.move();*/
// 代码合并
animals[i].move(); // 这个move()方法不是数组的。是数组当中Animal对象的move()方法。
}
// 动态初始化一个长度为2的Animal类型数组。
Animal[] ans = new Animal[2];
// 创建一个Animal对象,放到数组的第一个盒子中。
ans[0] = new Animal();
// Animal数组中只能存放Animal类型,不能存放Product类型。
//ans[1] = new Product();
// Animal数组中可以存放Cat类型的数据,因为Cat是一个Animal。
// Cat是Animal的子类。
ans[1] = new Cat();
// 创建一个Animal类型的数组,数组当中存储Cat和Bird
Cat c = new Cat();
Bird b = new Bird();
Animal[] anis = {c, b};
//Animal[] anis = {new Cat(), new Bird()}; // 该数组中存储了两个对象的内存地址。
for (int i = 0; i < anis.length; i++){
// 这个取出来的可能是Cat,也可能是Bird,不过肯定是一个Animal
// 如果调用的方法是父类中存在的方法不需要向下转型。直接使用父类型引用调用即可。
//anis[i]
//Animal an = anis[i];
//an.move();
//Animal中没有sing()方法。
//anis[i].sing();
// 调用子对象特有方法的话,需要向下转型!!!
if(anis[i] instanceof Cat){
Cat cat = (Cat)anis[i];
cat.catchMouse();
}else if(anis[i] instanceof Bird){
Bird bird = (Bird)anis[i];
bird.sing();
}
}
}
}
class Animal{
public void move(){
System.out.println("Animal move...");
}
}
// 商品类
class Product{
}
// Cat是子类
class Cat extends Animal {
public void move(){
System.out.println("猫在走猫步!");
}
// 特有方法
public void catchMouse(){
System.out.println("猫抓老鼠!");
}
}
// Bird子类
class Bird extends Animal {
public void move(){
System.out.println("Bird Fly!!!");
}
// 特有的方法
public void sing(){
System.out.println("鸟儿在歌唱!!!");
}
}
2.1.6 数组扩容&数组拷贝
004-数组拷贝内存图
/**
* 关于一维数组的扩容。
* 在java开发中,数组长度一旦确定不可变,那么数组满了怎么办?
* 数组满了,需要扩容。
* java中对数组的扩容是:
* 先新建一个大容量的数组,然后将小容量数组中的数据一个一个拷贝到大数组当中。
*
* 结论:数组扩容效率较低。因为涉及到拷贝的问题。所以在以后的开发中请注意:尽可能少的进行数组的拷贝。
* 可以在创建数组对象的时候预估计一下多长合适,最好预估准确,这样可以减少数组的扩容次数。提高效率。
*/
public class ArrayTest08 {
public static void main(String[] args) {
// java中的数组是怎么进行拷贝的呢?
//System.arraycopy(5个参数);
// 拷贝源(从这个数组中拷贝)
int[] src = {1, 11, 22, 3, 4};
// 拷贝目标(拷贝到这个目标数组上)
int[] dest = new int[20]; // 动态初始化一个长度为20的数组,每一个元素默认值0
// 调用JDK System类中的arraycopy方法,来完成数组的拷贝
//System.arraycopy(src, 1, dest, 3, 2);
// 遍历目标数组
/*
for (int i = 0; i < dest.length; i++) {
System.out.println(dest[i]); // 0 0 0 11 22 ... 0
}
*/
// 拷贝全部数据
System.arraycopy(src, 0, dest, 0, src.length);
for (int i = 0; i < dest.length; i++) {
System.out.println(dest[i]);
}
// 数组中如果存储的元素是引用,可以拷贝吗?当然可以。
String[] strs = {"hello", "world!", "study", "java", "oracle", "mysql", "jdbc"};
String[] newStrs = new String[20];
System.arraycopy(strs, 0, newStrs, 0, strs.length);
for (int i = 0; i < newStrs.length; i++) {
System.out.println(newStrs[i]);
}
System.out.println("================================");
Object[] objs = {new Object(), new Object(), new Object()};
Object[] newObjs = new Object[5];
// 思考一下:这里拷贝的时候是拷贝对象,还是拷贝对象的地址。(地址。)
System.arraycopy(objs, 0, newObjs, 0, objs.length);
for (int i = 0; i < newObjs.length; i++) {
System.out.println(newObjs[i]);
}
}
}
2.2 二维数组的声明和使用
二维数组属于多维数组,那么什么是多维数组呢,当数组元素的类型是数组时就成了多维数组,二维数组的声明格式如下:
1.数组元素的数据类型[][] 变量名;
2.数组元素的数据类型 变量名[][];
其中方括号的个数就是数组的维数,声明二维数组如下:
int[][] data;
2.2.1 二维数组的length属性
/*
关于java中的二维数组
1、二维数组其实是一个特殊的一维数组,特殊在这个一维数组当中的每一个元素是一个一维数组。
2、三维数组是什么?
三维数组是一个特殊的二维数组,特殊在这个二维数组中每一个元素是一个一维数组。
实际的开发中使用最多的就是一维数组。二维数组也很少使用。三维数组几乎不用。
3、二维数组静态初始化
int[][] array = {{1,1,1},{2,3,4,5},{0,0,0,0},{2,3,4,5},{2,3,4,5},{2,3,4,5},{2,3,4,5}};
*/
public class ArrayTest09 {
public static void main(String[] args) {
// 一维数组
int[] array = {100, 200, 300};
System.out.println(array.length); // 3
System.out.println("=======================");
// 二维数组
// 以下代码当中:里面的是4个一维数组。
int[][] a = {
{100, 200, 300},
{30, 20, 40, 50, 60},
{6, 7, 9, 1},
{0}
};
System.out.println(a.length); // 4
System.out.println(a[0].length); // 3
System.out.println(a[1].length); // 5
System.out.println(a[2].length); // 4
System.out.println(a[3].length); // 1
// 里面的是5个一维数组。
int[][] a2 = {
{100, 200, 300},
{30, 20, 40, 50, 60},
{6, 7, 9, 1},
{0},
{1,2,3,4,5}
};
}
}
2.2.2 二维数组的元素访问
/*
关于二维数组中元素的:读和改。
a[二维数组中的一维数组的下标][一维数组的下标]
a[0][0]:表示第1个一维数组中的第1个元素。
a[3][100]:表示第4个一维数组中的第101个元素。
注意:对于a[3][100]来说,其中 a[3] 是一个整体。[100]是前面a[3]执行结束的结果然后再下标100.
*/
public class ArrayTest10 {
public static void main(String[] args) {
// 二维数组
int[][] a = {
{34,4,65},
{100,200,3900,111},
{0}
};
// 请取出以上二位数中的第1个一维数组。
int[] 我是第1个一维数组 = a[0];
int 我是第1个一维数组中的第1个元素 = 我是第1个一维数组[0];
System.out.println(我是第1个一维数组中的第1个元素);
// 以下代码的由来是因为以上代码的合并导致的。
System.out.println(a[0][0]);
// 取出第2个一维数组当中第3个元素
System.out.println("第二个一维数组中第三个元素:" + a[1][2]);
// 取出第3个一维数组当中第1个元素
System.out.println("第3个一维数组中第1个元素:" + a[2][0]);
// 改
a[2][0] = 11111;
System.out.println(a[2][0]);
// 注意别越界。
//java.lang.ArrayIndexOutOfBoundsException
//System.out.println(a[2][1]);
}
}
方法的参数是一个二维数组
/*
动态初始化二维数组。
*/
public class ArrayTest12 {
public static void main(String[] args) {
// 3行4列。
// 3个一维数组,每一个一维数组当中4个元素。
int[][] array = new int[3][4];
// 二维数组遍历
/*
for (int i = 0; i < array.length; i++) { // 循环3次。
for (int j = 0; j < array[i].length; j++) {
System.out.print(array[i][j] + " ");
}
System.out.println();
}
*/
// 静态初始化
int[][] a = {{1,2,3,4},{4,5,6,76},{1,23,4}};
printArray(a);
// 没有这种语法
//printArray({{1,2,3,4},{4,5,6,76},{1,23,4}});
// 可以这样写。
printArray(new int[][]{{1,2,3,4},{4,5,6,76},{1,23,4}});
}
public static void printArray(int[][] array){
// 遍历二维数组。
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
System.out.print(array[i][j] + " ");
}
System.out.println();
}
}
}
2.2.3 二维数组的遍历
/*
二维数组的遍历
*/
public class ArrayTest11 {
public static void main(String[] args) {
// 二维数组
String[][] array = {
{"java", "oracle", "c++", "python", "c#"},
{"张三", "李四", "王五"},
{"lucy", "jack", "rose"}
};
// 遍历二维数组
for(int i = 0; i < array.length; i++){ // 外层循环3次。(负责纵向。)
String[] 一维数组 = array[i];
// 负责遍历一维数组
for(int j = 0; j < 一维数组.length; j++){
System.out.print(一维数组[j] + " ");
}
// 输出换行符
System.out.println();
}
// 合并代码
for(int i = 0; i < array.length; i++){ // 外层循环3次。(负责纵向。)
for(int j = 0; j < array[i].length; j++){
System.out.print(array[i][j] + " ");
}
System.out.println();
}
}
}
2.3 作业题
第一题:
编写程序,使用一维数组,模拟栈数据结构。
要求:
1、这个栈可以存储java中的任何引用类型的数据。
2、在栈中提供push方法模拟压栈。(栈满了,要有提示信息。)
3、在栈中提供pop方法模拟弹栈。(栈空了,也有有提示信息。)
4、编写测试程序,new栈对象,调用push pop方法来模拟压栈弹栈的动作。
public class MyStack{ // 栈类
// 提供一个数组来存储栈中的元素
Object[] elements;
// 栈帧(永远指向栈顶部的元素)
// 每加1个元素,栈帧+1
// 每减1个元素,栈帧-1
int index;
// 构造方法
// 构造方法是不是应该给一维数组一个初始化容量。
// push方法(push方法压栈)
// 压栈表示栈中多一个元素。
// 但是栈如果已满,压栈失败。
// 这个方法的参数以及返回值类型自己定义。
// pop方法(pop方法弹栈)
// 弹栈表示栈中少一个元素。
// 但是栈如果已空,弹栈失败。
// 这个方法的参数以及返回值类型自己定义。
}
main(){
测试...
}
第二题:(java软件工程师人生路上第一个小型项目。锻炼一下面向对象。)
为某个酒店编写程序:酒店管理系统,模拟订房、退房、打印所有房间状态等功能。
1、该系统的用户是:酒店前台。
2、酒店使用一个二维数组来模拟。“Room[][] rooms;”
3、酒店中的每一个房间应该是一个java对象:Room
4、每一个房间Room应该有:房间编号、房间类型、房间是否空闲.
5、系统应该对外提供的功能:
可以预定房间:用户输入房间编号,订房。
可以退房:用户输入房间编号,退房。
可以查看所有房间的状态:用户输入某个指令应该可以查看所有房间状态。
2.3.1 数组模拟栈
// Object[] 这是一个万能的口袋,这个口袋中可以装任何引用数据类型的数据。
public class ArrayTest13 {
public static void main(String[] args) {
//String s = new String("fdsafdjsklafjdksl");
String s = "fdsafdjsklafjdksl";
int i = 10;
// 注意:"abc" 这是一个字符串对象,字符串在java中有优待,不需要new也是一个对象。
// "abc" 字符串也是java对象,属于String类型。
Object[] arr = {new Husband(), new Wife(), "abc"};
}
}
class Husband {
}
class Wife {
}
/*
编写程序,使用一维数组,模拟栈数据结构。
要求:
1、这个栈可以存储java中的任何引用类型的数据。
2、在栈中提供push方法模拟压栈。(栈满了,要有提示信息。)
3、在栈中提供pop方法模拟弹栈。(栈空了,也有有提示信息。)
4、编写测试程序,new栈对象,调用push pop方法来模拟压栈弹栈的动作。
5、假设栈的默认初始化容量是10.(请注意无参数构造方法的编写方式。)
*/
public class MyStack {
// 向栈当中存储元素,我们这里使用一维数组模拟。存到栈中,就表示存储到数组中。
// 因为数组是我们学习java的第一个容器。
// 为什么选择Object类型数组?因为这个栈可以存储java中的任何引用类型的数据
// new Animal()对象可以放进去,new Person()对象也可以放进去。因为Animal和Person的超级父类就是Object。
// 包括String也可以存储进去。因为String父类也是Object。
private Object[] elements;
// 栈帧,永远指向栈顶部元素
// 那么这个默认初始值应该是多少。注意:最初的栈是空的,一个元素都没有。
//private int index = 0; // 如果index采用0,表示栈帧指向了顶部元素的上方。
//private int index = -1; // 如果index采用-1,表示栈帧指向了顶部元素。
private int index;
/**
* 无参数构造方法。默认初始化栈容量10.
*/
public MyStack() {
// 一维数组动态初始化
// 默认初始化容量是10.
this.elements = new Object[10];
// 给index初始化
this.index = -1;
}
/**
* 压栈的方法
* @param obj 被压入的元素
*/
public void push(Object obj){
if(index >= elements.length - 1){
System.out.println("压栈失败,栈已满!");
return;
}
// 程序能够走到这里,说明栈没满
// 向栈中加1个元素,栈帧向上移动一个位置。
index++;
elements[index] = obj;
// 在声明一次:所有的System.out.println()方法执行时,如果输出引用的话,自动调用引用的toString()方法。
System.out.println("压栈" + obj + "元素成功,栈帧指向" + index);
}
/**
* 弹栈的方法,从数组中往外取元素。每取出一个元素,栈帧向下移动一位。
* @return
*/
public void pop(){
if(index < 0){
System.out.println("弹栈失败,栈已空!");
return;
}
// 程序能够执行到此处说明栈没有空。
System.out.print("弹栈" + elements[index] + "元素成功,");
// 栈帧向下移动一位。
index--;
System.out.println("栈帧指向" + index);
}
// set和get也许用不上,但是你必须写上,这是规矩。你使用IDEA生成就行了。
// 封装:第一步:属性私有化,第二步:对外提供set和get方法。
public Object[] getElements() {
return elements;
}
public void setElements(Object[] elements) {
this.elements = elements;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}
public class MyStackTest {
public static void main(String[] args) {
// 创建一个栈对象,初始化容量是10个。
MyStack stack = new MyStack();
// 调用方法压栈
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object()); // 最后压入的。最先弹出来。(这个才符合栈的数据结构。)
// 压这个元素失败了。
stack.push(new Object());
// 弹栈
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
}
}
2.3.2 酒店管理系统
005-酒店管理系统
/**
* 酒店的房间
*/
public class Room extends Object{
/**
* 房间编号
* 1楼:101 102 103 104 105 106..
* 2楼:201 202 203 204 205 206..
* 3楼:301 302 303 304 305 306..
* ...
*/
private int no;
/**
* 房间类型:标准间 单人间 总统套房
*/
private String type;
/**
* 房间状态。
* true表示空闲,房间可以被预定。
* false表示占用,房间不能被预定。
*/
private boolean status;
// 构造方法
public Room() {
}
public Room(int no, String type, boolean status) {
this.no = no;
this.type = type;
this.status = status;
}
// setter and getter
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
// IDEA工具对于boolean类型的变量,生成的get方法的方法名是:isXxx()
// 如果你不喜欢,可以修改为getXxx()
/*public boolean isStatus() {
return status;
}*/
public boolean getStatus() {
return status;
}
public void setStatus(boolean status) {
this.status = status;
}
// equals方法重写
// equals是用来比较两个对象是否相同的。
// 至于怎么比较,这个还是程序员自己定。
// 你认为两个房间的编号相同,就表示同一个房间,那么你写代码比较房间编号就行。
public boolean equals(Object obj) {
if(obj == null || !(obj instanceof Room)) return false;
if(this == obj) return true;
Room room = (Room)obj;
// 当前房间编号 等于 传过来的房间对象的房间编号。认为是同一个房间。
return this.getNo() == room.getNo();
}
// toString方法重写
// toString方法的目的就是将java对象转换成字符串形式。
// 怎么转,转换成什么格式,程序员自己定。目的就是:简单、清晰明了。
// 我不要看对象内存地址。我要看具体的信息。
public String toString() {
//return "[101,单人间,占用]";
//return "[102,单人间,空闲]"; // 写死了。
//动态(把一个变量塞到一个字符串当中,口诀:加一个双引号,双引号中间加两个加号,两个加号中间加变量名。)
return "["+no+","+type+","+(status ? "空闲" : "占用")+"]";
}
// 编写一个临时程序测试以下
// 一会可以删除这个main方法
/*
public static void main(String[] args) {
//Room room = new Room(101, "单人间", true);
Room room = new Room(101, "单人间", false);
//System.out.println(room.toString());
// room是一个引用
// println(引用),会自动调用引用的toString()方法。
System.out.println(room);
Room room1 = new Room(102, "单人间", false);
System.out.println(room.equals(room1));
}
*/
// 多行注释:ctrl + shift + /
// 查看一个类的属性和方法:ctrl + F12
}
/*
酒店对象,酒店中有二维数组,二维数组模拟大厦。
*/
public class Hotel {
/**
* 二维数组,模拟大厦所有的房间
*/
private Room[][] rooms;
// 盖楼通过构造方法来盖楼。
public Hotel(){
// 一共有几层,每层的房间类型是什么,每个房间的编号是什么。
// 我们可以先写死。一共三层、一层单人间、二层标准间、三层总统套房,每层有10个房间。
/**
* 房间编号
* 1楼:101 102 103 104 105 106..
* 2楼:201 202 203 204 205 206..
* 3楼:301 302 303 304 305 306..
* ...
*/
// 动态初始化
rooms = new Room[3][10]; // 3行10列。3层楼,每层10个房间。
// 创建30个Room对象,放到数组当中。
// 怎么放? 二维数组遍历。
for(int i = 0; i < rooms.length; i++){ // i是下标:0 1 2。i+1是楼层:1,2,3
for(int j = 0; j < rooms[i].length; j++){
if(i == 0){
// 一层
rooms[i][j] = new Room((i+1)*100+j+1, "单人间", true);
}else if(i == 1){
// 二层
rooms[i][j] = new Room((i+1)*100+j+1, "标准间", true);
}else if(i == 2){
// 三层
rooms[i][j] = new Room((i+1)*100+j+1, "总统套房", true);
}
}
}
}
// 在酒店对象上提供一个打印房间列表的方法
public void print(){
// 打印所有房间状态,就是遍历二维数组
for(int i = 0; i < rooms.length; i++){
// 里面for循环负责输出一层。
for(int j = 0; j < rooms[i].length; j++) {
Room room = rooms[i][j];
System.out.print(room);
}
// 换行
System.out.println();
}
}
/**
* 订房方法。
* @param roomNo 调用此方法时需要传递一个房间编号过来。这个房间编号是前台小姐姐输入的。
*/
public void order(int roomNo){
// 订房最主要的是将房间对象的status修改为false。
// Room对象的status修改为false。
// 假设房间编号207(下标是 rooms[1][6] )
// 通过房间编号演算出下标。获取房间对象。
Room room = rooms[roomNo / 100 - 1][roomNo % 100 - 1];
// 修改为占用。
room.setStatus(false);
System.out.println(roomNo + "已订房!");
}
/**
* 退房
* @param roomNo
*/
public void exit(int roomNo){
Room room = rooms[roomNo / 100 - 1][roomNo % 100 - 1];
// 修改为空闲。
room.setStatus(true);
System.out.println(roomNo + "已退房!");
}
}
import java.util.Scanner;
/*
为某个酒店编写程序:酒店管理系统,模拟订房、退房、打印所有房间状态等功能。
1、该系统的用户是:酒店前台。
2、酒店使用一个二维数组来模拟。“Room[][] rooms;”
3、酒店中的每一个房间应该是一个java对象:Room
4、每一个房间Room应该有:房间编号、房间类型、房间是否空闲.
5、系统应该对外提供的功能:
可以预定房间:用户输入房间编号,订房。
可以退房:用户输入房间编号,退房。
可以查看所有房间的状态:用户输入某个指令应该可以查看所有房间状态。
*/
public class HotelMgtSystem {
public static void main(String[] args) {
// 创建酒店对象
Hotel hotel = new Hotel();
// 打印房间状态
//hotel.print();
/*
首先输出一个欢迎页面
*/
System.out.println("欢迎使用酒店管理系统,请认真阅读以下使用说明");
System.out.println("功能编号对应的功能:[1]表示查看房间列表。[2]表示订房。[3]表示退房。[0]表示退出系统。");
Scanner s = new Scanner(System.in);
// 一直可以使用(死循环。)。
while(true){
System.out.print("请输入功能编号:");
int i = s.nextInt();
if(i == 1){
// 查看房间列表
hotel.print();
}else if(i == 2){
// 订房
System.out.print("请输入订房编号:");
int roomNo = s.nextInt(); //小姐姐输入房间编号
hotel.order(roomNo);
}else if(i == 3){
// 退房
System.out.print("请输入退房编号:");
int roomNo = s.nextInt(); //小姐姐输入房间编号
hotel.exit(roomNo);
}else if(i == 0){
// 退出系统
System.out.println("再见,欢迎下次再来!");
return;
}else{
// 出错了!
System.out.println("输入功能编号有误,请重新输入!");
}
}
}
}
2.3.3 武器数组作业
开放型题目,随意发挥:
写一个类Army,代表一支军队,这个类有一个属性Weapon数组w(用来存储该军队所拥有的所有武器),该类还提供一个构造方法,在构造方法里通过传一个int类型的参数来限定该类所能拥有的最大武器数量,并用这一大小来初始化数组w。
该类还提供一个方法addWeapon(Weapon wa),表示把参数wa所代表的武器加入到数组w中。
在这个类中还定义两个方法attackAll()让w数组中的所有武器攻击;以及moveAll()让w数组中的所有可移动的武器移动。
写一个主方法去测试以上程序。
提示:
Weapon是一个父类。应该有很多子武器。
这些子武器应该有一些是可移动的,有一些是可攻击的。
/**
* 可移动的接口
*/
public interface Moveable {
/**
* 移动行为
*/
void move();
}
/**
* 可射击的
*/
public interface Shootable {
/**
* 射击行为
*/
void shoot();
}
/**
* 所有武器的父类
*/
public class Weapon {
}
/**
* 坦克是一个武器,可移动,可攻击。
*/
public class Tank extends Weapon implements Moveable,Shootable{
@Override
public void move() {
System.out.println("坦克移动");
}
@Override
public void shoot() {
System.out.println("坦克开炮");
}
}
/**
* 高射炮,是武器,但是不能移动,只能射击
*/
public class GaoShePao extends Weapon implements Shootable {
@Override
public void shoot() {
System.out.println("高射炮开炮!!!");
}
}
/**
* 战斗机,是武器,可以移动,可攻击
*/
public class Fighter extends Weapon implements Moveable, Shootable {
@Override
public void move() {
System.out.println("战斗机起飞");
}
@Override
public void shoot() {
System.out.println("战斗机开炮!");
}
}
/**
* 物资运输飞机,是武器,但是只能移动,不能攻击
*/
public class WuZiFeiJi extends Weapon implements Moveable {
@Override
public void move() {
System.out.println("运输机起飞!");
}
}
类在强制类型转换过程中,如果是类转换成接口类型。那么类和接口之间不需要存在继承关系,也可以转换。
java语法中允许。
/**
* 军队
*
* 类在强制类型转换过程中,如果是类转换成接口类型。
* 那么类和接口之间不需要存在继承关系,也可以转换,
* java语法中允许。
*/
public class Army {
/**
* 武器数组
*/
private Weapon[] weapons;
/**
* 创建军队的构造方法。
* @param count 武器数量
*/
public Army(int count){
// 动态初始化数组中每一个元素默认值是null。
// 武器数组是有了,但是武器数组中没有放武器。
weapons = new Weapon[count];
}
/**
* 将武器加入数组
* @param weapon
*/
public void addWeapon(Weapon weapon) throws AddWeaponException {
for(int i = 0; i < weapons.length; i++){
//if(null == weapons[i]) {
if(weapons[i] == null) {
// 把武器添加到空的位置上。
weapons[i] = weapon;
System.out.println(weapon + ":武器添加成功");
return;
}
}
// 程序如果执行到此处说明,武器没有添加成功
throw new AddWeaponException("武器数量已达到上限!");
}
/**
* 所有可攻击的武器攻击。
*/
public void attackAll(){
// 遍历数组
for(int i = 0; i < weapons.length; i ++){
if(weapons[i] instanceof Shootable){
// 调用子类中特有的方法,向下转型。
Shootable shootable = (Shootable)weapons[i];
shootable.shoot();
}
}
}
/**
* 所有可移动的武器移动
*/
public void moveAll(){
// 遍历数组
for(int i = 0; i < weapons.length; i ++){
if(weapons[i] instanceof Moveable){
// 调用子类中特有的方法,向下转型。
Moveable moveable = (Moveable)weapons[i];
moveable.move();
}
}
}
}
/**
* 添加武器异常。
*/
public class AddWeaponException extends Exception {
public AddWeaponException(){
}
public AddWeaponException(String s){
super(s);
}
}
public class Test {
public static void main(String[] args) {
// 构建一个军队
Army army = new Army(4); // 军队只有4个武器。
// 创建武器对象
Fighter fighter = new Fighter();
Fighter fighter2 = new Fighter();
Tank tank = new Tank();
WuZiFeiJi wuZiFeiJi = new WuZiFeiJi();
GaoShePao gaoShePao = new GaoShePao();
// 添加武器
try {
army.addWeapon(fighter);
army.addWeapon(tank);
army.addWeapon(wuZiFeiJi);
army.addWeapon(gaoShePao);
army.addWeapon(fighter2);
} catch (AddWeaponException e) {
System.out.println(e.getMessage());
}
// 让所有可移动的移动
army.moveAll();
// 让所有可攻击的攻击
army.attackAll();
// 这是new一个异常对象。没有手动抛异常,它就是一个普通的java类。
// 就像User类一样。没有区别。
/*AddWeaponException e = new AddWeaponException("武器数量已达到上限!");
System.out.println(e.getMessage());*/
}
}
2.4 数组的排序
2.4.1 冒泡排序算法
/*
冒泡排序算法
1、每一次循环结束之后,都要找出最大的数据,放到参与比较的这堆数据的最右边。(冒出最大的那个气泡。)
2、核心:
拿着左边的数字和右边的数字比对,当左边 > 右边的时候,交换位置。
原始数据:
3, 2, 7, 6, 8
第1次循环:(最大的跑到最右边。)
2, 3, 7, 6, 8 (3和2比较,2 < 3,所以2和3交换位置)
2, 3, 7, 6, 8 (虽然不需要交换位置:但是3和7还是需要比较一次。)
2, 3, 6, 7, 8 (7和6交换位置)
2, 3, 6, 7, 8 (虽然不需要交换位置:但是3和7还是需要比较一次。)
经过第1次循环,此时剩下参与比较的数据:2, 3, 6, 7
第2次循环:
2, 3, 6, 7 (2和3比较,不需要交换位置)
2, 3, 6, 7 (3和6比较,不需要交换位置)
2, 3, 6, 7 (6和7比较,不需要交换位置)
经过第2次循环,此时剩下参与比较的数据:2, 3, 6
第3次循环:
2, 3, 6 (2和3比较,不需要交换位置)
2, 3, 6 (3和6比较,不需要交换位置)
经过第3次循环,此时剩下参与比较的数据:2, 3
第4次循环:
2, 3 (2和3比较,不需要交换位置)
*/
public class BubbleSort {
public static void main(String[] args) {
// 这是int类型的数组对象
//int[] arr = {3, 2, 7, 6, 8};
int[] arr = {9, 8, 10, 7, 6, 0, 11};
// 经过冒泡排序算法对以上数组中元素进行排序
// 冒泡排序算法的核心是什么?
// 7条数据,循环6次。以下的代码可以循环6次。
/*
for(int i = 0; i < arr.length-1; i++){
System.out.println(i);
}
*/
// 7条数据,循环6次。以下的代码可以循环6次。(冒泡排序的外层循环采用这种方式)
//int count = 0;
int count2 = 0;
for(int i = arr.length-1; i > 0; i--){
for(int j = 0; j < i; j++){
// 不管是否需要交换位置,总之是要比较一次的。
//count++;
if(arr[j] > arr[j+1]){
// 交换位置。
// arr[j] 和 arr[j+1] 交换
int temp;
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
count2++;
}
}
}
//System.out.println("比较次数:" + count);
System.out.println("交换位置的次数:" + count2); //13
// 输出结果
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
2.4.2 选择排序算法
/*
选择排序:
每一次从这堆“参与比较的数据当中”找出最小值,
拿着这个最小值和“参与比较的这堆最前面的元素”交换位置。
选择排序比冒泡排序好在:每一次的交换位置都是有意义的。
关键点:选择排序中的关键在于,你怎么找出一堆数据中最小的。
3 2 6 1 5
假设:
第一个3是最小的。
3和2比较,发现2更小,所以此时最小的是2.
继续拿着2往下比对,2和6比较,2仍然是最小的。
继续拿着2往下比对,2和1比对,发现1更小,所以此时最小的是1.
继续拿着1往下比对,1和5比对,发现1还是小的,所以1就是最小的。
拿着1和最左边的3交换位置。
2 6 3 5
假设:
第一个2是最小的。
...
6 3 5
假设6是最小的:
6和3比对,发现3更小,所以此时最小的是3.
...
*/
public class SelectSort {
public static void main(String[] args) {
//int[] arr = {3, 1, 6, 2, 5};
int[] arr = {9, 8, 10, 7, 6, 0, 11};
int count = 0;
int count2 = 0;
// 选择排序
// 5条数据循环4次。(外层循环4次。)
for(int i = 0; i < arr.length - 1; i++){
// i的值是0 1 2 3
// i正好是“参加比较的这堆数据中”最左边那个元素的下标。
//System.out.println(i);
// i是一个参与比较的这堆数据中的起点下标。
// 假设起点i下标位置上的元素是最小的。
int min = i;
for(int j = i+1; j < arr.length; j++){
count++;
//System.out.println("===>" + j);
if(arr[j] < arr[min]){
min = j; //最小值的元素下标是j
}
}
// 当i和min相等时,表示最初猜测是对的。
// 当i和min不相等时,表示最初猜测是错的,有比这个元素更小的元素,
// 需要拿着这个更小的元素和最左边的元素交换位置。
if(min != i){
// 表示存在更小的数据
// arr[min] 最小的数据
// arr[i] 最前面的数据
int temp;
temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
count2++;
}
}
// 冒泡排序和选择排序实际上比较的次数没变。
// 交换位置的次数减少了。
System.out.println("比较次数" + count); // 21
System.out.println("交换次数:" + count2); // 5
// 排序之后遍历
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
//1 2 3 4 5
//假设1是最小的,结果1确实是最小的,就不需要交换位置。
2.4.3 Arrays.sort的使用
import java.util.Arrays;
/**
* 使用以下SUN公司提供的数组工具类:java.util.Arrays;
*/
public class ArraysTest01 {
public static void main(String[] args) {
int[] arr = {112,3,4,56,67,1};
// 工具类当中的方法大部分都是静态的。
Arrays.sort(arr);
// 遍历输出
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
2.5 数组的搜索
/*
数组的元素查找
数组元素查找有两种方式:
第一种方式:一个一个挨着找,直到找到为止。
第二种方式:二分法查找(算法),这个效率较高。
*/
public class ArraySearch {
public static void main(String[] args) {
// 这个例子演示一下第一种方式
int[] arr = {4,5,5,87,8};
// 需求:找出87的下标。如果没有返回-1
// 一个一个挨着找。
/*
for(int i = 0; i < arr.length;i ++){
if(arr[i] == 87){
System.out.println("87元素的下标是:" + i);
return;
}
}
// 程序执行到此处,表示没有87
System.out.println("87不存在该元素!");
*/
// 最好以上的程序封装一个方法,思考:传什么参数?返回什么值?
// 传什么:第一个参数是数组,第二个参数是被查找的元素。
// 返回值:返回被查找的这个元素的下标。如果找不到返回-1.
int index = arraySearch(arr, 5);
System.out.println(index == -1 ? "该元素不存在" : "该元素下标是:" + index);
}
/**
* 从数组中检索某个元素的下标(返回的是第一个元素的下标。)
* @param arr 被检索的数组
* @param ele 被检索的元素
* @return 大于等于0的数表示元素的下标,-1表示该元素不存在
*/
public static int arraySearch(int[] arr, int ele) {
for (int i = 0; i < arr.length; i++) {
if(ele == arr[i]){
return i;
}
}
return -1;
}
}
2.5.1 二分法(折半法)查找
/*
1、数组工具类:自己写的。不是SUN的。
2、关于查找算法中的:二分法查找。
10(下标0) 11 12 13 14 15 16 17 18 19 20(下标10) arr数组。
通过二分法查找,找出18这个元素的下标:
(0 + 10) / 2 --> 中间元素的下标: 5
拿着中间这个元素和目标要查找的元素进行对比:
中间元素是:arr[5] --> 15
15 < 18(被查找的元素)
被查找的元素18在目前中间元素15的右边。
所以开始元素的下标从0变成 5 + 1.
再重新计算一个中间元素的下标:
开始下标是:5 + 1
结束下标是:10
(6 + 10) / 2 --> 8
8下标对应的元素arr[8]是18
找到的中间元素正好和被找的的元素18相等,表示找到了:下标为8
二分法查找的终止条件:一直折半,直到中间的那个元素恰好是被查找的元素。
3、二分法查找算法是基于排序的基础之上。(没有排序的数据是无法查找的。)
*/
public class ArrayUtil {
public static void main(String[] args) {
int[] arr = {100,200,230,235,600,1000,2000,9999};
// 找出arr这个数组中200所在的下标。
// 调用方法
int index = binarySearch(arr, 230);
System.out.println(index == -1 ? "该元素不存在!" : "该元素下标" + index);
}
/**
* 从数组中查找目标元素的下标。
* @param arr 被查找的数组(这个必须是已经排序的。)
* @param dest 目标元素
* @return -1表示该元素不存在,其它表示返回该元素的下标。
*/
public static int binarySearch(int[] arr, int dest) {
// 开始下标
int begin = 0;
// 结束下标
int end = arr.length - 1;
// 开始元素的下标只要在结束元素下标的左边,就有机会继续循环。
while(begin <= end) {
// 中间元素下标
int mid = (begin + end) / 2;
if (arr[mid] == dest) {
return mid;
} else if (arr[mid] < dest) {
// 目标在“中间”的右边
// 开始元素下标需要发生变化(开始元素的下标需要重新赋值)
begin = mid + 1; // 一直增
} else {
// arr[mid] > dest
// 目标在“中间”的左边
// 修改结束元素的下标
end = mid - 1; // 一直减
}
}
return -1;
}
}
2.5.2 Arrays.binarySearch的使用
import java.util.Arrays;
/**
* 好消息:
* SUN公司已经为我们程序员写好了一个数组工具类。
* java.util.Arrays;
*/
public class ArraysTest02 {
public static void main(String[] args) {
// java.util.Arrays; 工具类中有哪些方法,我们开发的时候要参考API帮助文档
// 不要死记硬背。
int[] arr = {3,6,4,5,12,1,2,32,5,5};
// 排序
Arrays.sort(arr);
// 输出
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
// 二分法查找(建立在排序基础之上。)
int index = Arrays.binarySearch(arr, 5);
System.out.println(index == -1 ? "该元素不存在" : "该元素下标是:" + index);
}
}
2.6 day23课堂笔记
1、数组
1.1、数组的优点和缺点,并且要理解为什么。
第一:空间存储上,内存地址是连续的。
第二:每个元素占用的空间大小相同。
第三:知道首元素的内存地址。
第四:通过下标可以计算出偏移量。
通过一个数学表达式,就可以快速计算出某个下标位置上元素的内存地址,
直接通过内存地址定位,效率非常高。
优点:检索效率高。
缺点:随机增删效率较低,数组无法存储大数据量。
注意:数组最后一个元素的增删效率不受影响。
1.2、一维数组的静态初始化和动态初始化
静态初始化:
int[] arr = {1,2,3,4};
Object[] objs = {new Object(), new Object(), new Object()};
动态初始化:
int[] arr = new int[4]; // 4个长度,每个元素默认值0
Object[] objs = new Object[4]; // 4个长度,每个元素默认值null
1.3、一维数组的遍历
for(int i = 0; i < arr.length; i++){
System.out.println(arr[i]);
}
1.4、二维数组的静态初始化和动态初始化
静态初始化:
int[][] arr = {
{1,2,34},
{54,4,34,3},
{2,34,4,5}
};
Object[][] arr = {
{new Object(),new Object()},
{new Object(),new Object()},
{new Object(),new Object(),new Object()}
};
动态初始化:
int[][] arr = new int[3][4];
Object[][] arr = new Object[4][4];
Animal[][] arr = new Animal[3][4];
// Person类型数组,里面可以存储Person类型对象,以及Person类型的子类型都可以。
Person[][] arr = new Person[2][2];
....
1.5、二维数组的遍历
for(int i = 0; i < arr.length; i++){ // 外层for循环负责遍历外面的一维数组。
// 里面这个for循环负责遍历二维数组里面的一维数组。
for(int j = 0; j < arr[i].length; j++){
System.out.print(arr[i][j]);
}
// 换行。
System.out.println();
}
1.6、main方法上“String[] args”参数的使用(非重点,了解一下,以后一般都是有界面的,用户可以在界面上输入用户名和密码等参数信息。)
1.7、数组的拷贝:System.arraycopy()方法的使用
数组有一个特点:长度一旦确定,不可变。
所以数组长度不够的时候,需要扩容,扩容的机制是:新建一个大数组,将小数组中的数据拷贝到大数组,然后小数组对象被垃圾回收。
1.8、对数组中存储引用数据类型的情况,要会画它的内存结构图。
2.7 day24课堂笔记
1、解决昨天的两个作业题:
第一题:数组模拟栈。
第二题:酒店管理系统的模拟。
2、数组
2.1、常见的算法:
排序算法:
冒泡排序算法
选择排序算法
查找算法:
二分法查找
以上算法在以后的java实际开发中我们不需要使用的。因为java已经封装好了,直接调用就行。只不过以后面试的时候,可能会有机会碰上。
2.2、算法实际上在java中不需要精通,因为java中已经封装好了,要排序就调用方法就行。
例如:java中提供了一个数组工具类:
java.util.Arrays
Arrays是一个工具类。
其中有一个sort()方法,可以排序。静态方法,直接使用类名调用就行。
3、冒泡排序:
参与比较的数据:9 8 10 7 6 0 11
第1次循环:
8 9 10 7 6 0 11 (第1次比较:交换)
8 9 10 7 6 0 11 (第2次比较:不交换)
8 9 7 10 6 0 11 (第3次比较:交换)
8 9 7 6 10 0 11 (第4次比较:交换)
8 9 7 6 0 10 11 (第5次比较:交换)
8 9 7 6 0 10 11 (第6次比较:不交换)
最终冒出的最大数据在右边:11
参与比较的数据:8 9 7 6 0 10
第2次循环:
8 9 7 6 0 10(第1次比较:不交换)
8 7 9 6 0 10(第2次比较:交换)
8 7 6 9 0 10(第3次比较:交换)
8 7 6 0 9 10(第4次比较:交换)
8 7 6 0 9 10(第5次比较:不交换)
参与比较的数据:8 7 6 0 9
第3次循环:
7 8 6 0 9(第1次比较:交换)
7 6 8 0 9(第2次比较:交换)
7 6 0 8 9(第3次比较:交换)
7 6 0 8 9(第4次比较:不交换)
参与比较的数据:7 6 0 8
第4次循环:
6 7 0 8(第1次比较:交换)
6 0 7 8(第2次比较:交换)
6 0 7 8(第3次比较:不交换)
参与比较的数据:6 0 7
第5次循环:
0 6 7(第1次比较:交换)
0 6 7(第2次比较:不交换)
参与比较的数据:0 6
第6次循环:
0 6 (第1次比较:不交换)
for(int i = 6; i > 0; i--){ // 6次
//7条数据比6次
//6条数据比5次
//5条数据比4次
//4条数据比3次
//3条数据比2次
//2条数据比1次
for(int j = 0; j < i; j++){
}
}
4、选择排序:
选择排序比冒泡排序的效率高。
高在交换位置的次数上。
选择排序的交换位置是有意义的。
循环一次,然后找出参加比较的这堆数据中最小的,拿着这个最小的值和最前面的数据“交换位置”。
参与比较的数据:3 1 6 2 5 (这一堆参加比较的数据中最左边的元素下标是0)
第1次循环之后的结果是:
1 3 6 2 5
参与比较的数据:3 6 2 5 (这一堆参加比较的数据中最左边的元素下标是1)
第2次循环之后的结果是:
2 6 3 5
参与比较的数据:6 3 5 (这一堆参加比较的数据中最左边的元素下标是2)
第3次循环之后的结果是:
3 6 5
参与比较的数据:6 5 (这一堆参加比较的数据中最左边的元素下标是3)
第4次循环之后的结果是:
5 6
注意:5条数据,循环4次。
5、二分法查找(折半查找):
第一:二分法查找建立在排序的基础之上。
第二:二分法查找效率要高于“一个挨着一个”的这种查找方式。
第三:二分法查找原理?
10(0下标) 23 56 89 100 111 222 235 500 600(下标9) arr数组
目标:找出600的下标
(0 + 9) / 2 --> 4(中间元素的下标)
arr[4]这个元素就是中间元素:arr[4]是 100
100 < 600
说明被查找的元素在100的右边。
那么此时开始下标变成:4 + 1
(5 + 9) / 2 --> 7(中间元素的下标)
arr[7] 对应的是:235
235 < 600
说明被查找的元素在235的右边。
开始下标又进行了转变:7 + 1
(8 + 9) / 2 --> 8
arr[8] --> 500
500 < 600
开始元素的下标又发生了变化:8 + 1
(9 + 9) / 2 --> 9
arr[9]是600,正好和600相等,此时找到了。
6、介绍一下java.util.Arrays工具类。
所有方法都是静态的,直接用类名调用
主要使用的是两个方法:
二分法查找,排序
以后要看文档,不要死记硬背。
03 常用类
3.1 String
3.1.1 String字符串的存储原理
讲义:
String类是不可变类,也就是说String对象声明后,将不可修改
从以上内存图,大家可以看到,String对象赋值后不能再修改,这就是不可变对象,如果对字符串修改,那么将会创建新的对象
注意:只要采用双引号赋值字符串,那么在编译期将会放到方法区中的字符串的常量池里,如果是运行时对字符串相加或相减会放到堆中(放之前会先验证方法区中是否含有相同的字符串常量,如果存在,把地址返回,如果不存在,先将字符串常量放到池中,然后再返回该对象的地址)
001-String的内存图
/*
关于Java JDK中内置的一个类:java.lang.String
1、String表示字符串类型,属于引用数据类型,不属于基本数据类型。
2、在java中随便使用双引号括起来的都是String对象。例如:"abc","def","hello world!",这是3个String对象。
3、java中规定,双引号括起来的字符串,是不可变的,也就是说"abc"自出生到最终死亡,不可变,不能变成"abcd",也不能变成"ab"
4、在JDK当中双引号括起来的字符串,例如:"abc" "def"都是直接存储在“方法区”的“字符串常量池”当中的。
为什么SUN公司把字符串存储在一个“字符串常量池”当中呢。因为字符串在实际的开发中使用太频繁。为了执行效率,
所以把字符串放到了方法区的字符串常量池当中。
*/
public class StringTest01 {
public static void main(String[] args) {
// 这两行代码表示底层创建了3个字符串对象,都在字符串常量池当中。
String s1 = "abcdef";
String s2 = "abcdef" + "xy";
// 分析:这是使用new的方式创建的字符串对象。这个代码中的"xy"是从哪里来的?
// 凡是双引号括起来的都在字符串常量池中有一份。
// new对象的时候一定在堆内存当中开辟空间。
String s3 = new String("xy");
// i变量中保存的是100这个值。
int i = 100;
// s变量中保存的是字符串对象的内存地址。
// s引用中保存的不是"abc",是0x1111
// 而0x1111是"abc"字符串对象在“字符串常量池”当中的内存地址。
String s = "abc";
}
}
public class User {
private int id;
private String name;
public User() {
}
public User(int id, String name) {
this.id = id;
this.name = name;
}
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 class UserTest {
public static void main(String[] args) {
User user = new User(110, "张三");
}
}
002-String类型的引用中同样也是保存了对象的内存地址
public class StringTest02 {
public static void main(String[] args) {
String s1 = "hello";
// "hello"是存储在方法区的字符串常量池当中
// 所以这个"hello"不会新建。(因为这个对象已经存在了!)
String s2 = "hello";
// 分析结果是true还是false?
// == 双等号比较的是不是变量中保存的内存地址?是的。
System.out.println(s1 == s2); // true
String x = new String("xyz");
String y = new String("xyz");
// 分析结果是true还是false?
// == 双等号比较的是不是变量中保存的内存地址?是的。
System.out.println(x == y); //false
// 通过这个案例的学习,我们知道了,字符串对象之间的比较不能使用“==”
// "=="不保险。应该调用String类的equals方法。
// String类已经重写了equals方法,以下的equals方法调用的是String重写之后的equals方法。
System.out.println(x.equals(y)); // true
String k = new String("testString");
//String k = null;
// "testString"这个字符串可以后面加"."呢?
// 因为"testString"是一个String字符串对象。只要是对象都能调用方法。
System.out.println("testString".equals(k)); // 建议使用这种方式,因为这个可以避免空指针异常。
System.out.println(k.equals("testString")); // 存在空指针异常的风险。不建议这样写。
}
}
003-String相关面试题
3.1.2 String面试题创建了几个对象
/*
分析以下程序,一共创建了几个对象
*/
public class StringTest03 {
public static void main(String[] args) {
/*
一共3个对象:
方法区字符串常量池中有1个:"hello"
堆内存当中有两个String对象。
一共3个。
*/
String s1 = new String("hello");
String s2 = new String("hello");
}
}
通过以上分析,大家会看到创建了3个对象,堆区中2个,常量池中1一个
通过以上分析,使用String时,不建议使用new关键字,因为使用new会创建两个对象
记住:堆区中是运行期分配的,常量池中是编译器分配的
3.1.3 String常用方法简介
1.endsWith:判断字符串是否以指定的后缀结束
2.startsWith,判断字符串是否以指定的前缀开始
3.equals,字符串相等比较,不忽略大小写
4.equalsIgnoreCase,字符串相等比较,忽略大小写
5.indexOf,取得指定字符在字符串的位置
6.lastIndexOf,返回最后一次字符串出现的位置
7.length,取得字符串的长度
8.replaceAll,替换字符串中指定的内容
9.split,根据指定的表达式拆分字符串
10.substring,截子串
11.trim,去前尾空格
12.valueOf,将其他类型转换成字符串
3.1.3.4 String类的构造方法
/**
* 关于String类中的构造方法。
* 第一个:String s = new String("");
* 第二个:String s = ""; 最常用
* 第三个:String s = new String(char数组);
* 第四个:String s = new String(char数组,起始下标,长度);
* 第五个:String s = new String(byte数组);
* 第六个:String s = new String(byte数组,起始下标,长度)
*/
public class StringTest04 {
public static void main(String[] args) {
// 创建字符串对象最常用的一种方式
String s1 = "hello world!";
// s1这个变量中保存的是一个内存地址。
// 按说以下应该输出一个地址。
// 但是输出一个字符串,说明String类已经重写了toString()方法。
System.out.println(s1);//hello world!
System.out.println(s1.toString()); //hello world!
// 这里只掌握常用的构造方法。
byte[] bytes = {97, 98, 99}; // 97是a,98是b,99是c
String s2 = new String(bytes);
// 前面说过:输出一个引用的时候,会自动调用toString()方法,默认Object的话,会自动输出对象的内存地址。
// 通过输出结果我们得出一个结论:String类已经重写了toString()方法。
// 输出字符串对象的话,输出的不是对象的内存地址,而是字符串本身。
System.out.println(s2.toString()); //abc
System.out.println(s2); //abc
// String(字节数组,数组元素下标的起始位置,长度)
// 将byte数组中的一部分转换成字符串。
String s3 = new String(bytes, 1, 2);
System.out.println(s3); // bc
// 将char数组全部转换成字符串
char[] chars = {'我','是','中','国','人'};
String s4 = new String(chars);
System.out.println(s4);
// 将char数组的一部分转换成字符串
String s5 = new String(chars, 2, 3);
System.out.println(s5);
String s6 = new String("helloworld!");
System.out.println(s6); //helloworld!
}
}
3.1.3.5 String类常用方法
public class StringTest05 {
public static void main(String[] args) {
// String类当中常用方法。
//1(掌握).char charAt(int index)
char c = "中国人".charAt(1); // "中国人"是一个字符串String对象。只要是对象就能“点.”
System.out.println(c); // 国
// 2(了解).int compareTo(String anotherString)
// 字符串之间比较大小不能直接使用 > < ,需要使用compareTo方法。
int result = "abc".compareTo("abc");
System.out.println(result); //0(等于0) 前后一致 10 - 10 = 0
int result2 = "abcd".compareTo("abce");
System.out.println(result2); //-1(小于0) 前小后大 8 - 9 = -1
int result3 = "abce".compareTo("abcd");
System.out.println(result3); // 1(大于0) 前大后小 9 - 8 = 1
// 拿着字符串第一个字母和后面字符串的第一个字母比较。能分胜负就不再比较了。
System.out.println("xyz".compareTo("yxz")); // -1
// 3(掌握).boolean contains(CharSequence s)
// 判断前面的字符串中是否包含后面的子字符串。
System.out.println("HelloWorld.java".contains(".java")); // true
System.out.println("http://www.baidu.com".contains("https://")); // false
// 4(掌握). boolean endsWith(String suffix)
// 判断当前字符串是否以某个子字符串结尾。
System.out.println("test.txt".endsWith(".java")); // false
System.out.println("test.txt".endsWith(".txt")); // true
System.out.println("fdsajklfhdkjlsahfjkdsahjklfdss".endsWith("ss")); // true
// 5(掌握).boolean equals(Object anObject)
// 比较两个字符串必须使用equals方法,不能使用“==”
// equals方法有没有调用compareTo方法? 老版本可以看一下。JDK13中并没有调用compareTo()方法。
// equals只能看出相等不相等。
// compareTo方法可以看出是否相等,并且同时还可以看出谁大谁小。
System.out.println("abc".equals("abc")); // true
// 6(掌握).boolean equalsIgnoreCase(String anotherString)
// 判断两个字符串是否相等,并且同时忽略大小写。
System.out.println("ABc".equalsIgnoreCase("abC")); // true
// 7(掌握).byte[] getBytes()
// 将字符串对象转换成字节数组
byte[] bytes = "abcdef".getBytes();
for(int i = 0; i < bytes.length; i++){
System.out.println(bytes[i]);
}
// 8(掌握).int indexOf(String str)
// 判断某个子字符串在当前字符串中第一次出现处的索引(下标)。
System.out.println("oraclejavac++.netc#phppythonjavaoraclec++".indexOf("java")); // 6
// 9(掌握).boolean isEmpty()
// 判断某个字符串是否为“空字符串”。底层源代码调用的应该是字符串的length()方法。
//String s = "";
String s = "a";
System.out.println(s.isEmpty());
// 10(掌握). int length()
// 面试题:判断数组长度和判断字符串长度不一样
// 判断数组长度是length属性,判断字符串长度是length()方法。
System.out.println("abc".length()); // 3
System.out.println("".length()); // 0
// 11(掌握).int lastIndexOf(String str)
// 判断某个子字符串在当前字符串中最后一次出现的索引(下标)
System.out.println("oraclejavac++javac#phpjavapython".lastIndexOf("java")); //22
// 12(掌握). String replace(CharSequence target, CharSequence replacement)
// 替换。
// String的父接口就是:CharSequence
String newString = "http://www.baidu.com".replace("http://", "https://");
System.out.println(newString); //https://www.baidu.com
// 把以下字符串中的“=”替换成“:”
String newString2 = "name=zhangsan&password=123&age=20".replace("=", ":");
System.out.println(newString2); //name:zhangsan&password:123&age:20
// 13(掌握).String[] split(String regex)
// 拆分字符串
String[] ymd = "1980-10-11".split("-"); //"1980-10-11"以"-"分隔符进行拆分。
for(int i = 0; i < ymd.length; i++){
System.out.println(ymd[i]);
}
String param = "name=zhangsan&password=123&age=20";
String[] params = param.split("&");
for(int i = 0; i <params.length; i++){
System.out.println(params[i]);
// 可以继续向下拆分,可以通过“=”拆分。
}
// 14(掌握)、boolean startsWith(String prefix)
// 判断某个字符串是否以某个子字符串开始。
System.out.println("http://www.baidu.com".startsWith("http")); // true
System.out.println("http://www.baidu.com".startsWith("https")); // false
// 15(掌握)、 String substring(int beginIndex) 参数是起始下标。
// 截取字符串
System.out.println("http://www.baidu.com".substring(7)); //www.baidu.com
// 16(掌握)、String substring(int beginIndex, int endIndex)
// beginIndex起始位置(包括)
// endIndex结束位置(不包括)
System.out.println("http://www.baidu.com".substring(7, 10)); //www
// 17(掌握)、char[] toCharArray()
// 将字符串转换成char数组
char[] chars = "我是中国人".toCharArray();
for(int i = 0; i < chars.length; i++){
System.out.println(chars[i]);
}
// 18(掌握)、String toLowerCase()
// 转换为小写。
System.out.println("ABCDefKXyz".toLowerCase());
// 19(掌握)、String toUpperCase();
System.out.println("ABCDefKXyz".toUpperCase());
// 20(掌握). String trim();
// 去除字符串前后空白
System.out.println(" hello world ".trim());
// 21(掌握). String中只有一个方法是静态的,不需要new对象
// 这个方法叫做valueOf
// 作用:将“非字符串”转换成“字符串”
//String s1 = String.valueOf(true);
//String s1 = String.valueOf(100);
//String s1 = String.valueOf(3.14);
// 这个静态的valueOf()方法,参数是一个对象的时候,会自动调用该对象的toString()方法吗?
String s1 = String.valueOf(new Customer());
//System.out.println(s1); // 没有重写toString()方法之前是对象内存地址 com.bjpowernode.javase.string.Customer@10f87f48
System.out.println(s1); //我是一个VIP客户!!!!
// 我们是不是可以研究一下println()方法的源代码了?
System.out.println(100);
System.out.println(3.14);
System.out.println(true);
Object obj = new Object();
// 通过源代码可以看出:为什么输出一个引用的时候,会调用toString()方法!!!!
// 本质上System.out.println()这个方法在输出任何数据的时候都是先转换成字符串,再输出。
System.out.println(obj);
System.out.println(new Customer());
}
}
class Customer {
// 重写toString()方法
@Override
public String toString() {
return "我是一个VIP客户!!!!";
}
}
3.1.4 正则表达式初步
正则表达式(独立的学科),主要可以用来做字符串处理,可以描述特定的字符模式,如:”a{2}”表示由两个字符“a”构成的字符串,等同于普通字符串“aa”,如”\d”代表任意一个数字0~9,\D代表所有的非数字,\w代表所有的英文字母,\W代表所有的非英文字母。
3.2 StringBuffer和StringBuild
3.2.1 StringBuffer
StringBuffer称为字符串缓冲区,它的工作原理是:预先申请一块内存,存放字符序列,如果字符序列满了,会重新改变缓存区的大小,以容纳更多的字符序列。StringBuffer是可变对象,这个是String最大的不同
/**
* 思考:我们在实际的开发中,如果需要进行字符串的频繁拼接,会有什么问题?
* 因为java中的字符串是不可变的,每一次拼接都会产生新字符串。
* 这样会占用大量的方法区内存。造成内存空间的浪费。
* String s = "abc";
* s += "hello";
* 就以上两行代码,就导致在方法区字符串常量池当中创建了3个对象:
* "abc"
* "hello"
* "abchello"
*/
public class StringBufferTest01 {
public static void main(String[] args) {
String s = "";
// 这样做会给java的方法区字符串常量池带来很大的压力。
for(int i = 0; i < 100; i++){
//s += i;
s = s + i;
System.out.println(s);
}
}
}
/**
* 如果以后需要进行大量字符串的拼接操作,建议使用JDK中自带的:
* java.lang.StringBuffer
* java.lang.StringBuilder
*
* 如何优化StringBuffer的性能?
* 在创建StringBuffer的时候尽可能给定一个初始化容量。
* 最好减少底层数组的扩容次数。预估计一下,给一个大一些初始化容量。
* 关键点:给一个合适的初始化容量。可以提高程序的执行效率。
*/
public class StringBufferTest02 {
public static void main(String[] args) {
// 创建一个初始化容量为16个byte[] 数组。(字符串缓冲区对象)
StringBuffer stringBuffer = new StringBuffer();
// 拼接字符串,以后拼接字符串统一调用 append()方法。
// append是追加的意思。
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("d");
stringBuffer.append(3.14);
stringBuffer.append(true);
// append方法底层在进行追加的时候,如果byte数组满了,会自动扩容。
stringBuffer.append(100L);
System.out.println(stringBuffer.toString());
// 指定初始化容量的StringBuffer对象(字符串缓冲区对象)
StringBuffer sb = new StringBuffer(100);
sb.append("hello");
sb.append("world");
sb.append("hello");
sb.append("kitty");
System.out.println(sb);
}
}
3.2.2 StringBuilder
用法同StringBuffer,StringBuilder和StringBuffer的区别是StringBuffer中所有的方法都是同步的,是线程安全的,但速度慢,StringBuilder的速度快,但不是线程安全的
/*
java.lang.StringBuilder
StringBuffer和StringBuilder的区别?
StringBuffer中的方法都有:synchronized关键字修饰。表示StringBuffer在多线程环境下运行是安全的。
StringBuilder中的方法都没有:synchronized关键字修饰,表示StringBuilder在多线程环境下运行是不安全的。
StringBuffer是线程安全的。
StringBuilder是非线程安全的。
*/
public class StringBuilderTest01 {
public static void main(String[] args) {
// 使用StringBuilder也是可以完成字符串的拼接。
StringBuilder sb = new StringBuilder();
sb.append(100);
sb.append(true);
sb.append("hello");
sb.append("kitty");
System.out.println(sb);
}
}
3.2.3 面试题:String为什么是不可变的
/*
1、面试题:String为什么是不可变的?
我看过源代码,String类中有一个byte[]数组,这个byte[]数组采用了final修饰,
因为数组一旦创建长度不可变。并且被final修饰的引用一旦指向某个对象之后,不
可再指向其它对象,所以String是不可变的!
"abc" 无法变成 "abcd"
2、StringBuilder/StringBuffer为什么是可变的呢?
我看过源代码,StringBuffer/StringBuilder内部实际上是一个byte[]数组,
这个byte[]数组没有被final修饰,StringBuffer/StringBuilder的初始化
容量我记得应该是16,当存满之后会进行扩容,底层调用了数组拷贝的方法
System.arraycopy()...是这样扩容的。所以StringBuilder/StringBuffer
适合于使用字符串的频繁拼接操作。
*/
public class StringBufferTest04 {
public static void main(String[] args) {
// 字符串不可变是什么意思?
// 是说双引号里面的字符串对象一旦创建不可变。
String s = "abc"; //"abc"放到了字符串常量池当中。"abc"不可变。
// s变量是可以指向其它对象的。
// 字符串不可变不是说以上变量s不可变。说的是"abc"这个对象不可变。
s = "xyz";//"xyz"放到了字符串常量池当中。"xyz"不可变。
}
}
3.3 基本类型对应的包装类
3.3.1 包装类概述
基本类型的包装类主要提供了更多的实用操作,这样更容易处理基本类型。所有的包装类都是final的,所以不能创建其子类,包装类都是不可变对象
除了boolean和Character外,其它的包装类都有valueOf()和parseXXX方法,并且还具有byteVaue(),shortVaue(),intValue(),longValue(),floatValue()和doubleValue()方法,这些方法是最常用的方法
在JDK5.0以前,包装类和基本类型做运算时,必须将包装类转换成基本类型才可以,而JDK5.0提供Auto-boxing/unboxing(自动装箱和拆箱)
自动将基础类型转换为对象(自动装箱)
自动将对象转换为基础类型(自动拆箱)
/*
1、java中为8种基本数据类型又对应准备了8种包装类型。8种包装类属于引用数据类型,父类是Object。
2、思考:为什么要再提供8种包装类呢?
因为8种基本数据类型不够用。
所以SUN又提供对应的8种包装类型。
*/
public class IntegerTest01 {
//入口
public static void main(String[] args) {
// 有没有这种需求:调用doSome()方法的时候需要传一个数字进去。
// 但是数字属于基本数据类型,而doSome()方法参数的类型是Object。
// 可见doSome()方法无法接收基本数据类型的数字。那怎么办呢?可以传一个数字对应的包装类进去。
// 把100这个数字经过构造方法包装成对象。
MyInt myInt = new MyInt(100);
// doSome()方法虽然不能直接传100,但是可以传一个100对应的包装类型。
doSome(myInt);
}
public static void doSome(Object obj){
//System.out.println(obj);
System.out.println(obj.toString());
}
}
// 这种包装类目前是我自己写的。实际开发中我们不需要自己写。
// 8种基本数据类型对应的8种包装类,SUN公司已经写好了。我们直接用。
public class MyInt {
int value;
public MyInt() {
}
public MyInt(int value) {
this.value = value;
}
@Override
public String toString() {
return String.valueOf(value);
}
}
3.3.2 自动装箱和自动拆箱
/*
1、8种基本数据类型对应的包装类型名是什么?
基本数据类型 包装类型
-------------------------------------
byte java.lang.Byte(父类Number)
short java.lang.Short(父类Number)
int java.lang.Integer(父类Number)
long java.lang.Long(父类Number)
float java.lang.Float(父类Number)
double java.lang.Double(父类Number)
boolean java.lang.Boolean(父类Object)
char java.lang.Character(父类Object)
2、以上八种包装类中,重点以java.lang.Integer为代表进行学习,其它的类型照葫芦画瓢就行。
3、八种包装类中其中6个都是数字对应的包装类,他们的父类都是Number,可以先研究一下Number中公共的方法:
Number是一个抽象类,无法实例化对象。
Number类中有这样的方法:
byte byteValue() 以 byte 形式返回指定的数值。
abstract double doubleValue()以 double 形式返回指定的数值。
abstract float floatValue()以 float 形式返回指定的数值。
abstract int intValue()以 int 形式返回指定的数值。
abstract long longValue()以 long 形式返回指定的数值。
short shortValue()以 short 形式返回指定的数值。
这些方法其实所有的数字包装类的子类都有,这些方法是负责拆箱的。
*/
public class IntegerTest02 {
public static void main(String[] args) {
// 123这个基本数据类型,进行构造方法的包装达到了:基本数据类型向引用数据类型的转换。
// 基本数据类型 -(转换为)->引用数据类型(装箱)
Integer i = new Integer(123);
// 将引用数据类型--(转换为)-> 基本数据类型
float f = i.floatValue();
System.out.println(f); //123.0
// 将引用数据类型--(转换为)-> 基本数据类型(拆箱)
int retValue = i.intValue();
System.out.println(retValue); //123
}
}
/*
关于Integer类的构造方法,有两个:
Integer(int)
Integer(String)
*/
public class IntegerTest03 {
public static void main(String[] args) {
// Java9之后不建议使用这个构造方法了。出现横线表示已过时。
// 将数字100转换成Integer包装类型(int --> Integer)
Integer x = new Integer(100);
System.out.println(x);
// 将String类型的数字,转换成Integer包装类型。(String --> Integer)
Integer y = new Integer("123");
System.out.println(y);
// double -->Double
Double d = new Double(1.23);
System.out.println(d);
// String --> Double
Double e = new Double("3.14");
System.out.println(e);
}
}
public class IntegerTest04 {
public static void main(String[] args) {
// 通过访问包装类的常量,来获取最大值和最小值
System.out.println("int的最大值:" + Integer.MAX_VALUE);
System.out.println("int的最小值:" + Integer.MIN_VALUE);
System.out.println("byte的最大值:" + Byte.MAX_VALUE);
System.out.println("byte的最小值:" + Byte.MIN_VALUE);
}
}
/**
* 好消息:在java5之后,引入了一种新特性,自动装箱和自动拆箱
* 自动装箱:基本数据类型自动转换成包装类。
* 自动拆箱:包装类自动转换成基本数据类型。
*
* 有了自动拆箱之后,Number类中的方法就用不着了!
*
* 自动装箱和自动拆箱的好处?
* 方便编程。
*/
public class IntegerTest05 {
public static void main(String[] args) {
// 900是基本数据类型
// x是包装类型
// 基本数据类型 --(自动转换)--> 包装类型:自动装箱
Integer x = 900;
System.out.println(x);
// x是包装类型
// y是基本数据类型
// 包装类型 --(自动转换)--> 基本数据类型:自动拆箱
int y = x;
System.out.println(y);
// z是一个引用,z是一个变量,z还是保存了一个对象的内存地址。
Integer z = 1000; // 等同于:Integer z = new Integer(1000);
// 分析为什么这个没有报错呢?
// +两边要求是基本数据类型的数字,z是包装类,不属于基本数据类型,这里会进行自动拆箱。将z转换成基本数据类型
// 在java5之前你这样写肯定编译器报错。
System.out.println(z + 1);
Integer a = 1000; // Integer a = new Integer(1000); a是个引用,保存内存地址指向对象。
Integer b = 1000; // Integer b = new Integer(1000); b是个引用,保存内存地址指向对象。
// == 比较的是对象的内存地址,a和b两个引用中保存的对象内存地址不同。
// == 这个运算符不会触发自动拆箱机制。(只有+ - * /等运算的时候才会。)
System.out.println(a == b); //false
}
}
/*
这个题目是Integer非常重要的面试题。
*/
public class IntegerTest06 {
public static void main(String[] args) {
Integer a = 128;
Integer b = 128;
System.out.println(a == b); //false
/*
java中为了提高程序的执行效率,将[-128到127]之间所有的包装对象提前创建好,
放到了一个方法区的“整数型常量池”当中了,目的是只要用这个区间的数据不需要
再new了,直接从整数型常量池当中取出来。
原理:x变量中保存的对象的内存地址和y变量中保存的对象的内存地址是一样的。
*/
Integer x = 127;
Integer y = 127;
// == 永远判断的都是两个对象的内存地址是否相同。
System.out.println(x == y); //true
}
}
004-Integer的内存结构
3.3.3 Integer常用方法
import jdk.swing.interop.SwingInterOpUtils;
/*
总结一下之前所学的经典异常?
空指针异常:NullPointerException
类型转换异常:ClassCastException
数组下标越界异常:ArrayIndexOutOfBoundsException
数字格式化异常:NumberFormatException
Integer类当中有哪些常用的方法呢?
*/
public class IntegerTest07 {
public static void main(String[] args) {
// 手动装箱
Integer x = new Integer(1000);
// 手动拆箱。
int y = x.intValue();
System.out.println(y);
Integer a = new Integer("123");
// 编译的时候没问题,一切符合java语法,运行时会不会出问题呢?
// 不是一个“数字”可以包装成Integer吗?不能。运行时出现异常。
// java.lang.NumberFormatException
//Integer a = new Integer("中文");
// 重点方法
// static int parseInt(String s)
// 静态方法,传参String,返回int
//网页上文本框中输入的100实际上是"100"字符串。后台数据库中要求存储100数字,此时java程序需要将"100"转换成100数字。
int retValue = Integer.parseInt("123"); // String -转换-> int
//int retValue = Integer.parseInt("中文"); // NumberFormatException
System.out.println(retValue + 100);
// 照葫芦画瓢
double retValue2 = Double.parseDouble("3.14");
System.out.println(retValue2 + 1); //4.140000000000001(精度问题)
float retValue3 = Float.parseFloat("1.0");
System.out.println(retValue3 + 1); //2.0
// -----------------------------------以下内容作为了解,不需要掌握---------------------------------------
// static String toBinaryString(int i)
// 静态的:将十进制转换成二进制字符串。
String binaryString = Integer.toBinaryString(3);
System.out.println(binaryString); //"11" 二进制字符串
// static String toHexString(int i)
// 静态的:将十进制转换成十六进制字符串。
String hexString = Integer.toHexString(16);
System.out.println(hexString); // "10"
// 十六进制:1 2 3 4 5 6 7 8 9 a b c d e f 10 11 12 13 14 15 16 17 18 19 1a
hexString = Integer.toHexString(17);
System.out.println(hexString); // "11"
//static String toOctalString(int i)
// 静态的:将十进制转换成八进制字符串。
String octalString = Integer.toOctalString(8);
System.out.println(octalString); // "10"
System.out.println(new Object()); //java.lang.Object@6e8cf4c6
// valueOf方法作为了解
//static Integer valueOf(int i)
// 静态的:int-->Integer
Integer i1 = Integer.valueOf(100);
System.out.println(i1);
// static Integer valueOf(String s)
// 静态的:String-->Integer
Integer i2 = Integer.valueOf("100");
System.out.println(i2);
}
}
3.3.4 String int Integer类型互换
005-String Integer int三种类型的互相转换
/**
* String int Integer之间互相转换
*/
public class IntegerTest08 {
public static void main(String[] args) {
// String --> int
int i1 = Integer.parseInt("100"); // i1是100数字
System.out.println(i1 + 1); // 101
// int --> String
String s2 = i1 + ""; // "100"字符串
System.out.println(s2 + 1); // "1001"
// int --> Integer
// 自动装箱
Integer x = 1000;
// Integer --> int
// 自动拆箱
int y = x;
// String --> Integer
Integer k = Integer.valueOf("123");
// Integer --> String
String e = String.valueOf(k);
}
}
3.4 日期类
常用日期类:
java.util.Date
java.text.SimpleDateFormat
java.util.Calendar
3.4.1 java对日期的处理
import java.text.SimpleDateFormat;
import java.util.Date;
/*
java中对日期的处理
这个案例最主要掌握:
知识点1:怎么获取系统当前时间
知识点2:String ---> Date
知识点3:Date ---> String
*/
public class DateTest01 {
public static void main(String[] args) throws Exception {
// 获取系统当前时间(精确到毫秒的系统当前时间)
// 直接调用无参数构造方法就行。
Date nowTime = new Date();
// java.util.Date类的toString()方法已经被重写了。
// 输出的应该不是一个对象的内存地址,应该是一个日期字符串。
//System.out.println(nowTime); //Thu Mar 05 10:51:06 CST 2020
// 日期可以格式化吗?
// 将日期类型Date,按照指定的格式进行转换:Date --转换成具有一定格式的日期字符串-->String
// SimpleDateFormat是java.text包下的。专门负责日期格式化的。
/*
yyyy 年(年是4位)
MM 月(月是2位)
dd 日
HH 时
mm 分
ss 秒
SSS 毫秒(毫秒3位,最高999。1000毫秒代表1秒)
注意:在日期格式中,除了y M d H m s S这些字符不能随便写之外,剩下的符号格式自己随意组织。
*/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
//SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
//SimpleDateFormat sdf = new SimpleDateFormat("yy/MM/dd HH:mm:ss");
String nowTimeStr = sdf.format(nowTime);
System.out.println(nowTimeStr);
// 假设现在有一个日期字符串String,怎么转换成Date类型?
// String --> Date
String time = "2008-08-08 08:08:08 888";
//SimpleDateFormat sdf2 = new SimpleDateFormat("格式不能随便写,要和日期字符串格式相同");
// 注意:字符串的日期格式和SimpleDateFormat对象指定的日期格式要一致。不然会出现异常:java.text.ParseException
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
Date dateTime = sdf2.parse(time);
System.out.println(dateTime); //Fri Aug 08 08:08:08 CST 2008
}
}
3.4.2 统计方法执行时长
System.currentTimeMillis()
/*
获取自1970年1月1日 00:00:00 000到当前系统时间的总毫秒数。
1秒 = 1000毫秒
简单总结一下System类的相关属性和方法:
System.out 【out是System类的静态变量。】
System.out.println() 【println()方法不是System类的,是PrintStream类的方法。】
System.gc() 建议启动垃圾回收器
System.currentTimeMillis() 获取自1970年1月1日到系统当前时间的总毫秒数。
System.exit(0) 退出JVM。
*/
public class DateTest02 {
public static void main(String[] args) {
// 获取自1970年1月1日 00:00:00 000到当前系统时间的总毫秒数。
long nowTimeMillis = System.currentTimeMillis();
System.out.println(nowTimeMillis); //1583377912981
// 统计一个方法耗时
// 在调用目标方法之前记录一个毫秒数
long begin = System.currentTimeMillis();
print();
// 在执行完目标方法之后记录一个毫秒数
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
// 需求:统计一个方法执行所耗费的时长
public static void print(){
for(int i = 0; i < 1000000000; i++){
System.out.println("i = " + i);
}
}
}
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateTest03 {
public static void main(String[] args) {
// 这个时间是什么时间?
// 1970-01-01 00:00:00 001
Date time = new Date(1); // 注意:参数是一个毫秒
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(time);
// 北京是东8区。差8个小时。
System.out.println(strTime); // 1970-01-01 08:00:00 001
// 获取昨天的此时的时间。
Date time2 = new Date(System.currentTimeMillis() - 1000 * 60 * 60 * 24);
String strTime2 = sdf.format(time2);
System.out.println(strTime2); //2020-03-04 11:44:14 829
// 获取“去年的今天”的时间
// 自己玩。
}
}
3.5 数字类
import java.text.DecimalFormat;
/*
关于数字的格式化。(了解)
*/
public class DecimalFormatTest01 {
public static void main(String[] args) {
// java.text.DecimalFormat专门负责数字格式化的。
//DecimalFormat df = new DecimalFormat("数字格式");
/*
数字格式有哪些?
# 代表任意数字
, 代表千分位
. 代表小数点
0 代表不够时补0
###,###.##
表示:加入千分位,保留2个小数。
*/
DecimalFormat df = new DecimalFormat("###,###.##");
//String s = df.format(1234.56);
String s = df.format(1234.561232);
System.out.println(s); // "1,234.56"
DecimalFormat df2 = new DecimalFormat("###,###.0000"); //保留4个小数位,不够补上0
String s2 = df2.format(1234.56);
System.out.println(s2); // "1,234.5600"
}
}
import java.math.BigDecimal;
/*
1、BigDecimal 属于大数据,精度极高。不属于基本数据类型,属于java对象(引用数据类型)
这是SUN提供的一个类。专门用在财务软件当中。
2、注意:财务软件中double是不够的。咱们之前有一个学生去用友面试,经理就问了这样一个问题:
你处理过财务数据吗?用的哪一种类型?
千万别说double,说java.math.BigDecimal
*/
public class BigDecimalTest01 {
public static void main(String[] args) {
// 这个100不是普通的100,是精度极高的100
BigDecimal v1 = new BigDecimal(100);
// 精度极高的200
BigDecimal v2 = new BigDecimal(200);
// 求和
// v1 + v2; // 这样不行,v1和v2都是引用,不能直接使用+求和。
BigDecimal v3 = v1.add(v2); // 调用方法求和。
System.out.println(v3); //300
BigDecimal v4 = v2.divide(v1);
System.out.println(v4); // 2
}
}
3.6 Random
import java.util.Random;
/**
* 随机数
*/
public class RandomTest01 {
public static void main(String[] args) {
// 创建随机数对象
Random random = new Random();
// 随机产生一个int类型取值范围内的数字。
int num1 = random.nextInt();
System.out.println(num1);
// 产生[0~100]之间的随机数。不能产生101。
// nextInt翻译为:下一个int类型的数据是101,表示只能取到100.
int num2 = random.nextInt(101); //不包括101
System.out.println(num2);
}
}
import java.util.Arrays;
import java.util.Random;
/*
编写程序,生成5个不重复的随机数[0-100]。重复的话重新生成。
最终生成的5个随机数放到数组中,要求数组中这5个随机数不重复。
*/
public class RandomTest02 {
public static void main(String[] args) {
// 创建Random对象
Random random = new Random();
// 准备一个长度为5的一维数组。
int[] arr = new int[5]; // 默认值都是0
for(int i = 0; i < arr.length; i++){
arr[i] = -1;
}
// 循环,生成随机数
int index = 0;
while(index < arr.length){
// 生成随机数
//int num = random.nextInt(101);
//int num = random.nextInt(6); // 只能生成[0-5]的随机数!
int num = random.nextInt(4); // 只能生成[0-3]的随机数!永远都有重复的,永远都凑不够5个。
System.out.println("生成的随机数:" + num);
// 判断arr数组中有没有这个num
// 如果没有这个num,就放进去。
if(!contains(arr, num)){
arr[index++] = num;
}
}
// 遍历以上的数组
for(int i = 0; i < arr.length; i++){
System.out.println(arr[i]);
}
}
/**
* 单独编写一个方法,这个方法专门用来判断数组中是否包含某个元素
* @param arr 数组
* @param key 元素
* @return true表示包含,false表示不包含。
*/
public static boolean contains(int[] arr, int key){
/*
// 这个方案bug。(排序出问题了。)
// 对数组进行升序
//Arrays.sort(arr);
// 进行二分法查找
// 二分法查找的结果 >= 0说明,这个元素找到了,找到了表示存在!
//return Arrays.binarySearch(arr, key) >= 0;
*/
for(int i = 0; i < arr.length; i++){
if(arr[i] == key){
// 条件成立了表示包含,返回true
return true;
}
}
// 这个就表示不包含!
return false;
}
}
3.7 Enum
/*
这个案例没有使用java中的枚举,分析以下程序,在设计方面有什么缺陷?
以下代码可以编译,也可以运行。这些都没有问题。
就是设计上你觉得有什么缺陷?
*/
public class EnumTest01 {
public static void main(String[] args) {
//System.out.println(10 / 0); //java.lang.ArithmeticException: / by zero
/*
int retValue = divide(10, 2);
System.out.println(retValue == 1 ? "计算成功" : "计算失败"); // 1
int retValue2 = divide(10, 0);
System.out.println(retValue2 == 0 ? "计算失败" : "计算成功"); // 0
*/
boolean success = divide(10, 0);
System.out.println(success ? "计算成功" : "计算失败");
}
/**
* 需求(这是设计者说的!):以下程序,计算两个int类型数据的商,计算成功返回1,计算失败返回0
* @param a int类型的数据
* @param b int类型的数据
* @return 返回1表示成功,返回0表示失败!
*/
/*
public static int divide(int a, int b){
try {
int c = a / b;
// 程序执行到此处表示以上代码没有发生异常。表示执行成功!
return 1;
} catch (Exception e){
// 程序执行到此处表示以上程序出现了异常!
// 表示执行失败!
return 0;
}
}
*/
// 设计缺陷?在这个方法的返回值类型上。返回一个int不恰当。
// 既然最后的结果只是成功和失败,最好使用布尔类型。因为布尔类型true和false正好可以表示两种不同的状态。
/*
public static int divide(int a, int b){
try {
int c = a / b;
// 返回10已经偏离了需求,实际上已经出错了,但是编译器没有检查出来。
// 我们一直想追求的是:所有的错误尽可能让编译器找出来,所有的错误越早发现越好!
return 10;
} catch (Exception e){
return 0;
}
}
*/
// 这种设计不错。
public static boolean divide(int a, int b){
try {
int c = a / b;
return true;
} catch (Exception e){
return false;
}
}
/*
思考:以上的这个方法设计没毛病,挺好,返回true和false表示两种情况,
但是在以后的开发中,有可能遇到一个方法的执行结果可能包括三种情况,
四种情况,五种情况不等,但是每一个都是可以数清楚的,一枚一枚都是可以
列举出来的。这个布尔类型就无法满足需求了。此时需要使用java语言中的
枚举类型。
*/
}
// 采用枚举的方式改造程序
/*
总结:
1、枚举是一种引用数据类型
2、枚举类型怎么定义,语法是?
enum 枚举类型名{
枚举值1,枚举值2
}
3、结果只有两种情况的,建议使用布尔类型。
结果超过两种并且还是可以一枚一枚列举出来的,建议使用枚举类型。
例如:颜色、四季、星期等都可以使用枚举类型。
*/
public class EnumTest02 {
public static void main(String[] args) {
Result r = divide(10, 2);
System.out.println(r == Result.SUCCESS ? "计算成功" : "计算失败");
}
/**
* 计算两个int类型数据的商。
* @param a int数据
* @param b int数据
* @return Result.SUCCESS表示成功,Result.FAIL表示失败!
*/
public static Result divide(int a, int b){
try {
int c = a / b;
return Result.SUCCESS;
} catch (Exception e){
return Result.FAIL;
}
}
}
// 枚举:一枚一枚可以列举出来的,才建议使用枚举类型。
// 枚举编译之后也是生成class文件。
// 枚举也是一种引用数据类型。
// 枚举中的每一个值可以看做是常量。
enum Result{
// SUCCESS 是枚举Result类型中的一个值
// FAIL 是枚举Result类型中的一个值
// 枚举中的每一个值,可以看做是“常量”
SUCCESS, FAIL
}
/**
* 四季枚举类型
*/
public enum Season {
/*
春夏秋冬
*/
SPRING, SUMMER, AUTUMN, WINTER
}
/**
* 颜色枚举类型
*/
public enum Color {
/**
* 颜色值
*/
RED,BLUE,YELLOW,BLACK
}
/*
class MyClass {
public static final String RED = "red";
public static final String BLUE = "blue";
public static final String YELLOW = "yellow";
public static final String BLACK = "black";
public static void main(String[] args) {
String c = MyClass.RED;
System.out.println(c);
// RED
System.out.println(Color.RED);
}
}
*/
public class SwitchTest {
public static void main(String[] args) {
// switch语句支持枚举类型
// switch也支持String、int
// 低版本的JDK,只支持int
// 高版本的JDK,支持int、String、枚举。
// byte short char也可以,因为存在自动类型转换。
switch (Season.SPRING) {
// 必须省略Season.
case SPRING:
System.out.println("春天");
break;
case SUMMER:
System.out.println("夏天");
break;
case AUTUMN:
System.out.println("秋天");
break;
case WINTER:
System.out.println("冬天");
break;
}
}
}
3.8 day25课堂笔记
1、String类。
1.1、对String在内存存储方面的理解:
第一:字符串一旦创建不可变。
第二:双引号括起来的字符串存储在字符串常量池中。
第三:字符串的比较必须使用equals方法。
第四:String已经重写了toString()和equals()方法。
1.2、String的构造方法。
String s = "abc";
String s = new String("abc");
String s = new String(byte数组);
String s = new String(byte数组, 起始下标, 长度);
String s = new String(char数组);
String s = new String(char数组, 起始下标, 长度);
1.3、String类常用的21个方法。
2、StringBuffer/StringBuilder
2.1、StringBuffer/StringBuilder可以看做可变长度字符串。
2.2、StringBuffer/StringBuilder初始化容量16.
2.3、StringBuffer/StringBuilder是完成字符串拼接操作的,方法名:append
2.4、StringBuffer是线程安全的。StringBuilder是非线程安全的。
2.5、频繁进行字符串拼接不建议使用“+”
3、八种基本数据类型对应的包装类
3.1、包装类存在有什么用?
方便编程(有的方法不能传数字,但可以传包装类进去)
3.2、八种包装类的类名是什么?
Byte
Short
Integer
Long
Float
Double
Boolean
Character
3.3、所有数字的父类Number
3.4、照葫芦画瓢:学习Integer,其它的模仿Integer。
3.5、什么是装箱?什么是拆箱?
3.9 day26课堂笔记
1、八种基本数据类型对应的包装类。
1.1、什么是自动装箱和自动拆箱,代码怎么写?
Integer x = 100; // x里面并不是保存100,保存的是100这个对象的内存地址。
Integer y = 100;
System.out.println(x == y); // true
Integer x = 128;
Integer y = 128;
System.out.println(x == y); // false
1.2、Integer类常用方法。
Integer.valueOf()
Integer.parseInt("123")
Integer.parseInt("中文") : NumberFormatException
1.3、Integer String int三种类型互相转换。
2、日期类
2.1、获取系统当前时间
Date d = new Date();
2.2、日期格式化:Date --> String
yyyy-MM-dd HH:mm:ss SSS
SimpleDateFormat sdf = new SimpleDate("yyyy-MM-dd HH:mm:ss SSS");
String s = sdf.format(new Date());
2.3、String --> Date
SimpleDateFormat sdf = new SimpleDate("yyyy-MM-dd HH:mm:ss");
Date d = sdf.parse("2008-08-08 08:08:08");
2.4、获取毫秒数
long begin = System.currentTimeMillis();
Date d = new Date(begin - 1000 * 60 * 60 * 24); //昨天的时间
3、数字类
3.1、DecimalFormat数字格式化
###,###.## 表示加入千分位,保留两个小数。
###,###.0000 表示加入千分位,保留4个小数,不够补0
3.2、BigDecimal
财务软件中通常使用BigDecimal
4、随机数
4.1、怎么产生int类型随机数。
Random r = new Random();
int i = r.nextInt();
4.2、怎么产生某个范围之内的int类型随机数。
Random r = new Random();
int i = r.nextInt(101); // 产生[0-100]的随机数。
5、枚举
5.1、枚举是一种引用数据类型。
5.2、枚举编译之后也是class文件。
5.3、枚举类型怎么定义?
enum 枚举类型名{
枚举值,枚举值2,枚举值3
}
5.4、当一个方法执行结果超过两种情况,并且是一枚一枚可以列举出来的时候,建议返回值类型设计为枚举类型。
04 异常处理
4.1 异常的基本概念
4.1.1 异常概述
/*
1、什么是异常,java提供异常处理机制有什么用?
以下程序执行过程中发生了不正常的情况,而这种不正常的情况叫做:异常
java语言是很完善的语言,提供了异常的处理方式,以下程序执行过程中出现了不正常情况,
java把该异常信息打印输出到控制台,供程序员参考。程序员看到异常信息之后,可以对
程序进行修改,让程序更加的健壮。
什么是异常:程序执行过程中的不正常情况。
异常的作用:增强程序的健壮性。
2、以下程序执行控制台出现了:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.bjpowernode.javase.exception.ExceptionTest01.main(ExceptionTest01.java:14)
这个信息被我们称为:异常信息。这个信息是JVM打印的。
*/
public class ExceptionTest01 {
public static void main(String[] args) {
int a = 10;
int b = 0;
// 实际上JVM在执行到此处的时候,会new异常对象:new ArithmeticException("/ by zero");
// 并且JVM将new的异常对象抛出,打印输出信息到控制台了。
int c = a / b;
System.out.println(a + "/" + b + "=" + c);
// 此处运行也会创建一个:ArithmeticException类型的异常对象。
//System.out.println(100 / 0);
// 我观察到异常信息之后,对程序进行修改,更加健壮。
/*
int a = 10;
int b = 2;
if(b == 0) {
System.out.println("除数不能为0");
return;
}
// 程序执行到此处表示除数一定不是0
int c = a / b;
System.out.println(a + "/" + b + "=" + c);
*/
}
}
4.1.2 Java中异常以类和对象形式存在
/*
java语言中异常是以什么形式存在的呢?
1、异常在java中以类的形式存在,每一个异常类都可以创建异常对象。
2、异常对应的现实生活中是怎样的?
火灾(异常类):
2008年8月8日,小明家着火了(异常对象)
2008年8月9日,小刚家着火了(异常对象)
2008年9月8日,小红家着火了(异常对象)
类是:模板。
对象是:实际存在的个体。
钱包丢了(异常类):
2008年1月8日,小明的钱包丢了(异常对象)
2008年1月9日,小芳的钱包丢了(异常对象)
....
*/
public class ExceptionTest02 {
public static void main(String[] args) {
// 通过“异常类”实例化“异常对象”
NumberFormatException nfe = new NumberFormatException("数字格式化异常!");
// java.lang.NumberFormatException: 数字格式化异常!
System.out.println(nfe);
// 通过“异常类”创建“异常对象”
NullPointerException npe = new NullPointerException("空指针异常发生了!");
//java.lang.NullPointerException: 空指针异常发生了!
System.out.println(npe);
}
}
4.1.3 异常的继承结构
异常的层次结构
错误
编译时异常又被称为:受检异常或者受控异常;运行时异常又被称为:未受检异常或者非受控异常
4.1.4 异常的分类
异常主要分为:错误、编译时异常(受控异常)、运行期异常(非受控异常)
异常都发生在运行阶段,所谓编译时异常表示必须在编写程序的时候预先对这种异常进行处理,如果不处理编译器报错。
错误:如果应用程序出现了Error,那么将无法恢复,只能重新启动应用程序,最典型的Error的异常是:OutOfMemoryError
编译时异常:出现了这种异常必须显示的处理,不显示处理java程序将无法编译通过
运行时异常:此种异常可以不用显示的处理,例如被0除异常,java没有要求我们一定要处理
4.2 异常的两种处理方式
第一种方式:在方法声明的位置上,使用throws关键字,抛给上一级。
谁调用我,我就抛给谁。抛给上一级。
第二种方式:使用try..catch语句进行异常的捕捉。
这件事发生了,谁也不知道,因为我给抓住了。
4.2.1 方法声明位置上使用throws
/*
以下代码报错的原因是什么?
因为doSome()方法声明位置上使用了:throws ClassNotFoundException
而ClassNotFoundException是编译时异常。必须编写代码时处理,没有处理
编译器报错。
*/
public class ExceptionTest04 {
public static void main(String[] args) {
// main方法中调用doSome()方法
// 因为doSome()方法声明位置上有:throws ClassNotFoundException
// 我们在调用doSome()方法的时候必须对这种异常进行预先的处理。
// 如果不处理,编译器就报错。
//编译器报错信息: Unhandled exception: java.lang.ClassNotFoundException
//doSome();
}
/**
* doSome方法在方法声明的位置上使用了:throws ClassNotFoundException
* 这个代码表示doSome()方法在执行过程中,有可能会出现ClassNotFoundException异常。
* 叫做类没找到异常。这个异常直接父类是:Exception,所以ClassNotFoundException属于编译时异常。
* @throws ClassNotFoundException
*/
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome!!!!");
}
}
4.2.2 try..catch进行捕捉
public class ExceptionTest05 {
// 第一种处理方式:在方法声明的位置上继续使用:throws,来完成异常的继续上抛。抛给调用者。
// 上抛类似于推卸责任。(继续把异常传递给调用者。)
/*
public static void main(String[] args) throws ClassNotFoundException {
doSome();
}
*/
// 第二种处理方式:try..catch进行捕捉。
// 捕捉等于把异常拦下了,异常真正的解决了。(调用者是不知道的。)
public static void main(String[] args) {
try {
doSome();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome!!!!");
}
}
4.2.3 异常捕捉和上报的联合使用
一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM。JVM只有终止。
异常处理机制的作用就是增强程序的健壮性。怎么能做到:异常发生了也不影响程序的执行。所以一般main方法中的异常建议使用try..catch进行捕捉。main就不要继续上抛了。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/*
处理异常的第一种方式:
在方法声明的位置上使用throws关键字抛出,谁调用我这个方法,我就抛给谁。抛给调用者来处理。
这种处理异常的态度:上报。
处理异常的第二种方式:
使用try..catch语句对异常进行捕捉。
这个异常不会上报,自己把这个事儿处理了。
异常抛到此处为止,不再上抛了。
注意:
只要异常没有捕捉,采用上报的方式,此方法的后续代码不会执行。
另外需要注意,try语句块中的某一行出现异常,该行后面的代码不会执行。
try..catch捕捉异常之后,后续代码可以执行。
在以后的开发中,处理编译时异常,应该上报还是捕捉呢,怎么选?
如果希望调用者来处理,选择throws上报。
其它情况使用捕捉的方式。
*/
public class ExceptionTest06 {
// 一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM。JVM只有终止。
// 异常处理机制的作用就是增强程序的健壮性。怎么能做到,异常发生了也不影响程序的执行。所以
// 一般main方法中的异常建议使用try..catch进行捕捉。main就不要继续上抛了。
/*
public static void main(String[] args) throws FileNotFoundException {
System.out.println("main begin");
m1();
System.out.println("main over");
}
*/
public static void main(String[] args) {
// 100 / 0这是算术异常,这个异常是运行时异常,你在编译阶段,可以处理,也可以不处理。编译器不管。
//System.out.println(100 / 0); // 不处理编译器也不管
// 你处理也可以。
/*
try {
System.out.println(100 / 0);
} catch(ArithmeticException e){
System.out.println("算术异常了!!!!");
}
*/
System.out.println("main begin");
try {
// try尝试
m1();
// 以上代码出现异常,直接进入catch语句块中执行。
System.out.println("hello world!");
} catch (FileNotFoundException e){ // catch后面的好像一个方法的形参。
// 这个分支中可以使用e引用,e引用保存的内存地址是那个new出来异常对象的内存地址。
// catch是捕捉异常之后走的分支。
// 在catch分支中干什么?处理异常。
System.out.println("文件不存在,可能路径错误,也可能该文件被删除了!");
System.out.println(e); //java.io.FileNotFoundException: D:\course\01-课\学习方法.txt (系统找不到指定的路径。)
}
// try..catch把异常抓住之后,这里的代码会继续执行。
System.out.println("main over");
}
private static void m1() throws FileNotFoundException {
System.out.println("m1 begin");
m2();
// 以上代码出异常,这里是无法执行的。
System.out.println("m1 over");
}
// 抛别的不行,抛ClassCastException说明你还是没有对FileNotFoundException进行处理
//private static void m2() throws ClassCastException{
// 抛FileNotFoundException的父对象IOException,这样是可以的。因为IOException包括FileNotFoundException
//private static void m2() throws IOException {
// 这样也可以,因为Exception包括所有的异常。
//private static void m2() throws Exception{
// throws后面也可以写多个异常,可以使用逗号隔开。
//private static void m2() throws ClassCastException, FileNotFoundException{
private static void m2() throws FileNotFoundException {
System.out.println("m2 begin");
// 编译器报错原因是:m3()方法声明位置上有:throws FileNotFoundException
// 我们在这里调用m3()没有对异常进行预处理,所以编译报错。
// m3();
m3();
// 以上如果出现异常,这里是无法执行的!
System.out.println("m2 over");
}
private static void m3() throws FileNotFoundException {
// 调用SUN jdk中某个类的构造方法。
// 这个类还没有接触过,后期IO流的时候就知道了。
// 我们只是借助这个类学习一下异常处理机制。
// 创建一个输入流对象,该流指向一个文件。
/*
编译报错的原因是什么?
第一:这里调用了一个构造方法:FileInputStream(String name)
第二:这个构造方法的声明位置上有:throws FileNotFoundException
第三:通过类的继承结构看到:FileNotFoundException父类是IOException,IOException的父类是Exception,
最终得知,FileNotFoundException是编译时异常。
错误原因?编译时异常要求程序员编写程序阶段必须对它进行处理,不处理编译器就报错。
*/
//new FileInputStream("D:\\course\\01-开课\\学习方法.txt");
// 我们采用第一种处理方式:在方法声明的位置上使用throws继续上抛。
// 一个方法体当中的代码出现异常之后,如果上报的话,此方法结束。
new FileInputStream("D:\\course\\01-课\\学习方法.txt");
System.out.println("如果以上代码出异常,这里会执行吗??????????????????不会!!!");
}
}
4.2.4 深入try..catch
1、catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型。
2、catch可以写多个。建议catch的时候,精确的一个一个处理。这样有利于程序的调试。
3、catch写多个的时候,从上到下,必须遵守从小到大。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/*
深入try..catch
1、catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型。
2、catch可以写多个。建议catch的时候,精确的一个一个处理。这样有利于程序的调试。
3、catch写多个的时候,从上到下,必须遵守从小到大。
*/
public class ExceptionTest07 {
/*
public static void main(String[] args) throws Exception, FileNotFoundException, NullPointerException {
}
*/
/*public static void main(String[] args) throws Exception {
}*/
public static void main(String[] args) {
//编译报错
/*try {
FileInputStream fis = new FileInputStream("D:\\course\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
} catch(NullPointerException e) {
}*/
/*try {
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
System.out.println("以上出现异常,这里无法执行!");
} catch(FileNotFoundException e) {
System.out.println("文件不存在!");
}
System.out.println("hello world!");*/
/*try {
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
} catch(IOException e) { // 多态:IOException e = new FileNotFoundException();
System.out.println("文件不存在!");
}*/
/*try {
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
} catch(Exception e) { // 多态:Exception e = new FileNotFoundException();
System.out.println("文件不存在!");
}*/
/*try {
//创建输入流
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
//读文件
fis.read();
} catch(Exception e) { //所有的异常都走这个分支。
System.out.println("文件不存在!");
}*/
/*try {
//创建输入流
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
//读文件
fis.read();
} catch(FileNotFoundException e) {
System.out.println("文件不存在!");
} catch(IOException e){
System.out.println("读文件报错了!");
}*/
// 编译报错。
/*
try {
//创建输入流
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
//读文件
fis.read();
} catch(IOException e){
System.out.println("读文件报错了!");
} catch(FileNotFoundException e) {
System.out.println("文件不存在!");
}
*/
// JDK8的新特性!
try {
//创建输入流
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
// 进行数学运算
System.out.println(100 / 0); // 这个异常是运行时异常,编写程序时可以处理,也可以不处理。
} catch(FileNotFoundException | ArithmeticException | NullPointerException e) {
System.out.println("文件不存在?数学异常?空指针异常?都有可能!");
}
}
}
4.3 getMessage和printStackTrace()
如何取得异常对象的具体信息,常用的方法主要有两种:
取得异常描述信息:getMessage()
取得异常的堆栈信息(比较适合于程序调试阶段):printStackTrace();
/*
异常对象有两个非常重要的方法:
获取异常简单的描述信息:
String msg = exception.getMessage();
打印异常追踪的堆栈信息:
exception.printStackTrace();
*/
public class ExceptionTest08 {
public static void main(String[] args) {
// 这里只是为了测试getMessage()方法和printStackTrace()方法。
// 这里只是new了异常对象,但是没有将异常对象抛出。JVM会认为这是一个普通的java对象。
NullPointerException e = new NullPointerException("空指针异常fdsafdsafdsafds");
// 获取异常简单描述信息:这个信息实际上就是构造方法上面String参数。
String msg = e.getMessage(); //空指针异常fdsafdsafdsafds
System.out.println(msg);
// 打印异常堆栈信息
// java后台打印异常堆栈追踪信息的时候,采用了异步线程的方式打印的。
e.printStackTrace();
for(int i = 0; i < 1000; i++){
System.out.println("i = " + i);
}
System.out.println("Hello World!");
}
}
import java.io.FileInputStream;
import java.io.FileNotFoundException;
/*
异常对象的两个方法:
String msg = e.getMessage();
e.printStackTrace(); // 一般都是使用这个。
我们以后查看异常的追踪信息,我们应该怎么看,可以快速的调试程序呢?
异常信息追踪信息,从上往下一行一行看。
但是需要注意的是:SUN写的代码就不用看了(看包名就知道是自己的还是SUN的。)。
主要的问题是出现在自己编写的代码上。
*/
public class ExceptionTest09 {
public static void main(String[] args) {
try {
m1();
} catch (FileNotFoundException e) {
// 获取异常的简单描述信息
String msg = e.getMessage();
System.out.println(msg); //C:\jetns-agent.jar (系统找不到指定的文件。)
//打印异常堆栈追踪信息!!!
//在实际的开发中,建议使用这个。养成好习惯!
// 这行代码要写上,不然出问题你也不知道!
//e.printStackTrace();
/*
java.io.FileNotFoundException: C:\jetns-agent.jar (系统找不到指定的文件。)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:155)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:110)
at com.bjpowernode.javase.exception.ExceptionTest09.m3(ExceptionTest09.java:31)
at com.bjpowernode.javase.exception.ExceptionTest09.m2(ExceptionTest09.java:27)
at com.bjpowernode.javase.exception.ExceptionTest09.m1(ExceptionTest09.java:23)
at com.bjpowernode.javase.exception.ExceptionTest09.main(ExceptionTest09.java:14)
因为31行出问题导致了27行
27行出问题导致23行
23行出问题导致14行。
应该先查看31行的代码。31行是代码错误的根源。
*/
}
// 这里程序不耽误执行,很健壮。《服务器不会因为遇到异常而宕机。》
System.out.println("Hello World!");
}
private static void m1() throws FileNotFoundException {
m2();
}
private static void m2() throws FileNotFoundException {
m3();
}
private static void m3() throws FileNotFoundException {
new FileInputStream("C:\\jetns-agent.jar");
}
}
4.4 finally关键字
4.4.1 finally子句的使用
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/*
关于try..catch中的finally子句:
1、在finally子句中的代码是最后执行的,并且是一定会执行的,即使try语句块中的代码出现了异常。
finally子句必须和try一起出现,不能单独编写。
2、finally语句通常使用在哪些情况下呢?
通常在finally语句块中完成资源的释放/关闭。
因为finally中的代码比较有保障。
即使try语句块中的代码出现异常,finally中代码也会正常执行。
*/
public class ExceptionTest10 {
public static void main(String[] args) {
FileInputStream fis = null; // 声明位置放到try外面。这样在finally中才能用。
try {
// 创建输入流对象
fis = new FileInputStream("D:\\course\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
// 开始读文件....
String s = null;
// 这里一定会出现空指针异常!
s.toString();
System.out.println("hello world!");
// 流使用完需要关闭,因为流是占用资源的。
// 即使以上程序出现异常,流也必须要关闭!
// 放在这里有可能流关不了。
//fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch(IOException e){
e.printStackTrace();
} catch(NullPointerException e) {
e.printStackTrace();
} finally {
System.out.println("hello 浩克!");
// 流的关闭放在这里比较保险。
// finally中的代码是一定会执行的。
// 即使try中出现了异常!
if (fis != null) { // 避免空指针异常!
try {
// close()方法有异常,采用捕捉的方式。
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("hello kitty!");
}
}
try不能单独使用。
tryfinally可以联合使用。
以下代码的执行顺序:
先执行try...
再执行finally...
最后执行return(return语句只要执行方法必然结束。)
/*
finally语句:
放在finally语句块中的代码是一定会执行的【再次强调!!!】
*/
public class ExceptionTest11 {
public static void main(String[] args) {
/*
try和finally,没有catch可以吗?可以。
try不能单独使用。
try finally可以联合使用。
以下代码的执行顺序:
先执行try...
再执行finally...
最后执行 return (return语句只要执行方法必然结束。)
*/
try {
System.out.println("try...");
return;
} finally {
// finally中的语句会执行。能执行到。
System.out.println("finally...");
}
// 这里不能写语句,因为这个代码是无法执行到的。
//System.out.println("Hello World!");
}
}
退出JVM,finally语句不执行
/*
finally
*/
public class ExceptionTest12 {
public static void main(String[] args) {
try {
System.out.println("try...");
// 退出JVM
System.exit(0); // 退出JVM之后,finally语句中的代码就不执行了!
} finally {
System.out.println("finally...");
}
}
}
4.4.2 finally面试题
/*
finally面试题
*/
public class ExceptionTest13 {
public static void main(String[] args) {
int result = m();
System.out.println(result); //100
}
/*
java语法规则(有一些规则是不能破坏的,一旦这么说了,就必须这么做!):
java中有一条这样的规则:
方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法!)
java中还有一条语法规则:
return语句一旦执行,整个方法必须结束(亘古不变的语法!)
*/
public static int m(){
int i = 100;
try {
// 这行代码出现在int i = 100;的下面,所以最终结果必须是返回100
// return语句还必须保证是最后执行的。一旦执行,整个方法结束。
return i;
} finally {
i++;
}
}
}
/*
反编译之后的效果
public static int m(){
int i = 100;
int j = i;
i++;
return j;
}
*/
4.4.3 final finally finalize的区别
/*
final finally finalize有什么区别?
final 关键字
final修饰的类无法继承
final修饰的方法无法覆盖
final修饰的变量不能重新赋值。
finally 关键字
和try一起联合使用。
finally语句块中的代码是必须执行的。
finalize 标识符
是一个Object类中的方法名。
这个方法是由垃圾回收器GC负责调用的。
*/
public class ExceptionTest14 {
public static void main(String[] args) {
// final是一个关键字。表示最终的。不变的。
final int i = 100;
//i = 200;
// finally也是一个关键字,和try联合使用,使用在异常处理机制中
// 在fianlly语句块中的代码是一定会执行的。
try {
} finally {
System.out.println("finally....");
}
// finalize()是Object类中的一个方法。作为方法名出现。
// 所以finalize是标识符。
// finalize()方法是JVM的GC垃圾回收器负责调用。
Object obj;
}
}
// final修饰的类无法继承
final class A {
// 常量。
public static final double MATH_PI = 3.1415926;
}
class B {
// final修饰的方法无法覆盖
public final void doSome(){
}
}
4.5 如何自定义异常
/*
1、SUN提供的JDK内置的异常肯定是不够的用的。在实际的开发中,有很多业务,
这些业务出现异常之后,JDK中都是没有的。和业务挂钩的。那么异常类我们
程序员可以自己定义吗?
可以。
2、Java中怎么自定义异常呢?
两步:
第一步:编写一个类继承Exception或者RuntimeException.
第二步:提供两个构造方法,一个无参数的,一个带有String参数的。
死记硬背。
*/
public class MyException extends Exception{ // 编译时异常
public MyException(){
}
public MyException(String s){
super(s);
}
}
/*
public class MyException extends RuntimeException{ // 运行时异常
}
*/
public class ExceptionTest15 {
public static void main(String[] args) {
// 创建异常对象(只new了异常对象,并没有手动抛出)
MyException e = new MyException("用户名不能为空!");
// 打印异常堆栈信息
e.printStackTrace();
// 获取异常简单描述信息
String msg = e.getMessage();
System.out.println(msg);
}
}
4.5.1 异常在实际开发中的作用★★★★★
/**
* 栈操作异常:自定义异常!
*/
public class MyStackOperationException extends Exception{ // 编译时异常!
public MyStackOperationException(){
}
public MyStackOperationException(String s){
super(s);
}
}
// 改良之前
//System.out.println("压栈失败,栈已满!");
// 改良之后
// 合并(手动抛出异常!)
throw new MyStackOperationException("压栈失败,栈已满!");
/*
编写程序,使用一维数组,模拟栈数据结构。
要求:
1、这个栈可以存储java中的任何引用类型的数据。
2、在栈中提供push方法模拟压栈。(栈满了,要有提示信息。)
3、在栈中提供pop方法模拟弹栈。(栈空了,也有有提示信息。)
4、编写测试程序,new栈对象,调用push pop方法来模拟压栈弹栈的动作。
5、假设栈的默认初始化容量是10.(请注意无参数构造方法的编写方式。)
*/
public class MyStack {
// 向栈当中存储元素,我们这里使用一维数组模拟。存到栈中,就表示存储到数组中。
// 因为数组是我们学习java的第一个容器。
// 为什么选择Object类型数组?因为这个栈可以存储java中的任何引用类型的数据
// new Animal()对象可以放进去,new Person()对象也可以放进去。因为Animal和Person的超级父类就是Object。
// 包括String也可以存储进去。因为String父类也是Object。
private Object[] elements;
// 栈帧,永远指向栈顶部元素
// 那么这个默认初始值应该是多少。注意:最初的栈是空的,一个元素都没有。
//private int index = 0; // 如果index采用0,表示栈帧指向了顶部元素的上方。
//private int index = -1; // 如果index采用-1,表示栈帧指向了顶部元素。
private int index;
/**
* 无参数构造方法。默认初始化栈容量10.
*/
public MyStack() {
// 一维数组动态初始化
// 默认初始化容量是10.
this.elements = new Object[10];
// 给index初始化
this.index = -1;
}
/**
* 压栈的方法
* @param obj 被压入的元素
*/
public void push(Object obj) throws MyStackOperationException {
if(index >= elements.length - 1){
// 改良之前
//System.out.println("压栈失败,栈已满!");
//return;
// 创建异常对象
//MyStackOperationException e = new MyStackOperationException("压栈失败,栈已满!");
// 手动将异常抛出去!
//throw e; //这里捕捉没有意义,自己new一个异常,自己捉,没有意义。栈已满这个信息你需要传递出去。
// 合并(手动抛出异常!)
throw new MyStackOperationException("压栈失败,栈已满!");
}
// 程序能够走到这里,说明栈没满
// 向栈中加1个元素,栈帧向上移动一个位置。
index++;
elements[index] = obj;
// 在声明一次:所有的System.out.println()方法执行时,如果输出引用的话,自动调用引用的toString()方法。
System.out.println("压栈" + obj + "元素成功,栈帧指向" + index);
}
/**
* 弹栈的方法,从数组中往外取元素。每取出一个元素,栈帧向下移动一位。
* @return
*/
public void pop() throws MyStackOperationException {
if(index < 0){
//System.out.println("弹栈失败,栈已空!");
//return;
throw new MyStackOperationException("弹栈失败,栈已空!");
}
// 程序能够执行到此处说明栈没有空。
System.out.print("弹栈" + elements[index] + "元素成功,");
// 栈帧向下移动一位。
index--;
System.out.println("栈帧指向" + index);
}
// set和get也许用不上,但是你必须写上,这是规矩。你使用IDEA生成就行了。
// 封装:第一步:属性私有化,第二步:对外提供set和get方法。
public Object[] getElements() {
return elements;
}
public void setElements(Object[] elements) {
this.elements = elements;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}
// 测试改良之后的MyStack
// 注意:最后这个例子,是异常最终要的案例。必须掌握。自定义异常在实际开发中的应用。
public class ExceptionTest16 {
public static void main(String[] args) {
// 创建栈对象
MyStack stack = new MyStack();
// 压栈
try {
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
// 这里栈满了
stack.push(new Object());
} catch (MyStackOperationException e) {
// 输出异常的简单信息。
System.out.println(e.getMessage());
}
// 弹栈
try {
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
// 弹栈失败
stack.pop();
} catch (MyStackOperationException e) {
System.out.println(e.getMessage());
}
}
}
4.6 方法覆盖与异常
/*
之前在讲解方法覆盖的时候,当时遗留了一个问题?
重写之后的方法不能比重写之前的方法抛出更多(更宽泛)的异常,可以更少。
*/
class Animal {
public void doSome(){
}
public void doOther() throws Exception{
}
}
class Cat extends Animal {
// 编译正常。
public void doSome() throws RuntimeException{
}
// 编译报错。
/*public void doSome() throws Exception{
}*/
// 编译正常。
/*public void doOther() {
}*/
// 编译正常。
/*public void doOther() throws Exception{
}*/
// 编译正常。
public void doOther() throws NullPointerException{
}
}
/*
总结异常中的关键字:
异常捕捉:
try
catch
finally
throws 在方法声明位置上使用,表示上报异常信息给调用者。
throw 手动抛出异常!
*/
4.7 异常作业
编写程序模拟用户注册:
1、程序开始执行时,提示用户输入“用户名”和“密码”信息。
2、输入信息之后,后台java程序模拟用户注册。
3、注册时用户名要求长度在[6-14]之间,小于或者大于都表示异常。
注意:
完成注册的方法放到一个单独的类中。
异常类自定义即可。
class UserService {
public void register(String username,String password){
//这个方法中完成注册!
}
}
编写main方法,在main方法中接收用户输入的信息,在main方法中调用UserService的register方法完成注册。
/*
用户业务类,处理用户相关的业务:例如登录、注册等功能。
*/
public class UserService {
/**
* 用户注册
* @param username 用户名
* @param password 密码
* @throws IllegalNameException 当用户名为null,或者用户名长度小于6,或者长度大于14,会出现该异常!
*/
public void register(String username, String password) throws IllegalNameException {
/*
引用等于null的这个判断最好放到所有条件的最前面。
*/
/*if(username == null || username.length() < 6 || username.length() > 14){
}*/
/*
再分享一个经验:username == null 不如写成 null == username
"abc".equals(username) 比 username.equals("abc") 好。
*/
/*if(null == username || username.length() < 6 || username.length() > 14){
}*/
if(null == username || username.length() < 6 || username.length() > 14) {
/*System.out.println("用户名不合法,长度必须在[6-14]之间");
return;*/
throw new IllegalNameException("用户名不合法,长度必须在[6-14]之间");
}
// 程序能够执行到此处说明,用户名合法
System.out.println("注册成功,欢迎["+username+"]");
}
}
/**
* 自定义异常
*/
public class IllegalNameException extends Exception {
public IllegalNameException(){
}
public IllegalNameException(String s){
super(s);
}
}
public class Test {
public static void main(String[] args) {
// 创建UserService对象
UserService userService = new UserService();
// 用户名和密码就不再从控制台接收了
try {
userService.register("jack", "123");
} catch (IllegalNameException e) {
//System.out.println(e.getMessage());
e.printStackTrace();
}
}
}
4.8 day27课堂笔记
0、异常处理机制
0.1、java中异常的作用是:增强程序健壮性。
0.2、java中异常以类和对象的形式存在。
1、java的异常处理机制
1.1、异常在java中以类和对象的形式存在。那么异常的继承结构是怎样的?
我们可以使用UML图来描述一下继承结构。
画UML图有很多工具,例如:Rational Rose(收费,画UML最专业,最标准的)、starUML等....
Object
Object下有Throwable(可抛出的)
Throwable下有两个分支:Error(不可处理,直接退出JVM)和Exception(可处理的)
Exception下有两个分支:
Exception的直接子类:编译时异常(要求程序员在编写程序阶段必须预先对这些异常进行处理,如果不处理编译器报错,因此得名编译时异常。)。
RuntimeException:运行时异常。(在编写程序阶段程序员可以预先处理,也可以不管,都行。)
1.2、编译时异常和运行时异常,都是发生在运行阶段。编译阶段异常是不会发生的。
编译时异常因为什么而得名?
因为编译时异常必须在编译(编写)阶段预先处理,如果不处理编译器报错,因此得名。
所有异常都是在运行阶段发生的。因为只有程序运行阶段才可以new对象。
因为异常的发生就是new异常对象。
1.3、编译时异常和运行时异常的区别?
编译时异常一般发生的概率比较高。
举个例子:
你看到外面下雨了,倾盆大雨的。
你出门之前会预料到:如果不打伞,我可能会生病(生病是一种异常)。
而且这个异常发生的概率很高,所以我们出门之前要拿一把伞。
“拿一把伞”就是对“生病异常”发生之前的一种处理方式。
对于一些发生概率较高的异常,需要在运行之前对其进行预处理。
运行时异常一般发生的概率比较低。
举个例子:
小明走在大街上,可能会被天上的飞机轮子砸到。被飞机轮子砸到也算一种异常,但是这种异常发生概率较低。
在出门之前你没必要提前对这种发生概率较低的异常进行预处理。如果你预处理这种异常,你将活的很累。
假设你在出门之前,你把能够发生的异常都预先处理,你这个人会更加的安全,但是你这个人活的很累。
假设java中没有对异常进行划分,没有分为:编译时异常和运行时异常,所有的异常都需要在编写程序阶段对其进行预处理,将是怎样的效果呢?
首先,如果这样的话,程序肯定是绝对的安全的。但是程序员编写程序太累,代码到处都是处理异常的代码。
1.4、编译时异常还有其他名字:
受检异常:CheckedException
受控异常
1.5、运行时异常还有其它名字:
未受检异常:UnCheckedException
非受控异常
1.6、再次强调:所有异常都是发生在运行阶段的。
1.7、Java语言中对异常的处理包括两种方式:
第一种方式:在方法声明的位置上,使用throws关键字,抛给上一级。
谁调用我,我就抛给谁。抛给上一级。
第二种方式:使用try..catch语句进行异常的捕捉。
这件事发生了,谁也不知道,因为我给抓住了。
举个例子:
我是某集团的一个销售员,因为我的失误,导致公司损失了1000元,“损失1000元”这可以看做是一个异常发生了。我有两种处理方式:
第一种方式:我把这件事告诉我的领导【异常上抛】
第二种方式:我自己掏腰包把这个钱补上。【异常的捕捉】
张三 --> 李四 ---> 王五 --> CEO
思考:
异常发生之后,如果我选择了上抛,抛给了我的调用者,调用者需要对这个异常继续处理,那么调用者处理这个异常同样有两种处理方式。
1.8、注意:Java中异常发生之后如果一直上抛,最终抛给了main方法,main方法继续向上抛,抛给了调用者JVM,JVM知道这个异常发生,只有一个结果。终止java程序的执行。
public class ExceptionTest03 {
public static void main(String[] args) {
/*
程序执行到此处发生了ArithmeticException异常,
底层new了一个ArithmeticException异常对象,
然后抛出了,由于是main方法调用了100 / 0,
所以这个异常ArithmeticException抛给了main方法,
main方法没有处理,将这个异常自动抛给了JVM。
JVM最终终止程序的执行。
ArithmeticException 继承 RuntimeException,属于运行时异常。
在编写程序阶段不需要对这种异常进行预先的处理。
*/
System.out.println(100 / 0);
// 这里的HelloWorld没有输出,没有执行。
System.out.println("Hello World!");
}
}
2、什么是UML?有什么用?
统一建模语言(Unified Modeling Language,UML)
UML是一种统一建模语言,一种图标式语言(画图的)
UML不是只有java中使用。只要是面向对象的编程语言,都有UML。
一般画UML图的都是软件架构师或者说是系统分析师。这些级别的人员使用的。
软件设计人员使用UML。
在UML图中可以描述类和类之间的关系,程序执行的流程,对象的状态等.
盖大楼和软件开发一样,一个道理。
盖楼之前,会先由建筑师画图纸。图纸上一个一个符号都是标准符号。这个图纸画完,只要是搞建筑的都能看懂,因为这个图纸上标注的这些符号都是一种“标准的语言”。
在java软件开发当中,软件分析师/设计师负责设计类,java软件开发人员必须要能看懂。