文章目录
面向对象OOP概述
- 对象 -> 类 :
- 字段/属性/变量
- 构造器
- 方法
- 关键字/修饰符
OOP
由类
为组成单位,
类
的内部由成员
构成,
成员包括:成员变量(Field) 与 成员方法(Method)
类
:相当于一个模板/蓝图
- 相同特征事物的抽象描述(抽象上的人)
对象
:通过这个蓝图 创建(new)出来的 “实例
(instance)” 。
- 可以
new
很多实例,每个实例都是独立的。- 对象是类的一个实例,必然具备该类事物的属性和行为(即方法)。
- (具体的人)
- OOP设计 -> 类的设计 -> 类的成员设计
一、对象内存相关
-
堆 Heap:
new 出来 对象的数据 都放这, 存储对象的数据 -
栈 Stack:
存放 引用类型 地址(指向堆空间数据)
以及 值类型 的具体值 -
方法区 Method Area:
常量、静态变量、class字节码…
二、类的成员之一:成员变量(Field)
2.1 如何声明成员变量
- 位置要求:必须在类中,方法外
- 修饰符 : private、缺省、protected、public 还有 static final
- 初始化值:可以显式赋值,也可以不赋值,使用默认值
4.当一个对象被创建时,会对其中各种类型的成员变量自动进行初始化赋值。
2.2 成员变量 与 局部变量
与类 有关 成员变量
根据类的创建而创建 , 类or 对象 消失而消失
在类内,方法外
与方法 有关 局部变量
方法执行到存在 , 方法执行完毕跟随方法一起消失
在类内的方法内
static
可以将成员变量分为两大类,静态变量
和非静态变量
。
其中静态变量又称为类变量,非静态变量又称为实例变量或者属性。
相同点:
- 变量声明的格式相同: 数据类型 变量名 = 初始化值
- 变量必须先声明、后初始化、再使用。
- 变量都有其对应的作用域。只在其作用域内是有效的
不同点:
- 声明位置和方式
- 实例变量:在类中方法外
- 局部变量:在方法体{}中或方法的形参列表、代码块中
- 在内存中存储的位置不同
- 实例变量:堆
- 局部变量:栈
- 生命周期
- 实例变量:和对象的生命周期一样,随着对象的创建而存在,随着对象被GC回收而消亡, 而且每一个对象的实例变量是独立的。
- 局部变量:和方法调用的生命周期一样,每一次方法被调用而在存在,随着方法执行的结束而消亡, 而且每一次方法调用都是独立。
- 作用域
- 实例变量:通过对象就可以使用,本类中直接调用,其他类中“对象.实例变量”
- 局部变量:出了作用域就不能使用
- 修饰符
- 实例变量:public,protected,private,final等
- 局部变量:final
- 默认值
- 实例变量:有默认值
- 局部变量:没有,必须手动初始化。其中的形参比较特殊,靠实参给它初始化。
三、类的成员之一:成员方法(Method)
方法
是类或对象行为特征的抽象,用来完成某个功能操作。在某些语言中也称为函数或过程。- 将功能封装为方法的目的是,可以实现代码重用,减少冗余,简化代码
- Java里的方法不能独立存在,所有的方法必须定义在类里。
3.1 方法调用内存分析
- 方法没有被调用的时候,都在
方法区
中的字节码文件
(.class)中存储。 - 方法被调用的时候,需要进入到
栈
内存中运行。- 方法每调用一次就会在栈中有一个
入栈
动作,即 - 给当前方法开辟一块独立的内存区域,用于存储当前方法的局部变量的值。
- 当方法执行结束后,会释放该内存,称为
出栈
,如果方法有返回值,就会把结果返回调用处,如果没有返回值,就直接结束,回到调用处继续执行下一条指令。
- 方法每调用一次就会在栈中有一个
- 栈结构:先进后出,后进先出。
3.2 方法的重载
方法名相同,参数列表不同(个数、类型),与修饰符、返回值类型无关
重载方法调用:
JVM通过方法的参数列表,调用匹配的方法。
先找个数、类型最匹配的
再找个数和类型可以兼容的,如果同时多个方法可以兼容将会报错
3.3 方法的重写
方法的重写 (override、overwrite)
@Override
子类覆盖父类的方法
- 子类重写的方法
必须
和父类被重写的方法具有相同的方法名称
、参数列表
- 子类重写的方法的返回值类型
不能大于
父类被重写的方法的返回值类型。(例如:Student < Person)。 - 子类重写的方法使用的访问权限
不能小于
父类被重写的方法的访问权限。(public > protected > 缺省 > private) - 子类方法抛出的异常不能大于父类被重写方法的异常
注意:
非static方法。
如果返回值类型是基本数据类型和void,那么必须是相同
① 父类私有方法不能重写 ② 跨包的父类缺省的方法也不能重写
3.4 可变个数的形参
在JDK 5.0 中提供了Varargs(variable number of arguments)机制。
即当定义一个方法时,形参的类型可以确定,但是形参的个数不确定,那么可以考虑使用可变个数的形参
。
格式:
//JDK5.0:采用可变个数形参来定义方法,传入多个同一类型变量
public static void test(int a ,String...books);
举例:
需求:n个字符串进行拼接,每一个字符串之间使用某字符进行分割,如果没有传入字符串,那么返回空字符串""
public class StringTools {
String concat(char seperator, String... args){
String str = "";
for (int i = 0; i < args.length; i++) {
if(i == 0){
str += args[i];
}else{
str += seperator + args[i];
}
}
return str;
}
}
//测试:
StringTools tools = new StringTools();
System.out.println(tools.concat('-'));
System.out.println(tools.concat('-',"hello"));
System.out.println(tools.concat('-',"hello","world"));
System.out.println(tools.concat('-',"hello","world","java"));
3.5 形参与实参:参数传递机制->值传递
Java里方法的参数传递方式只有一种:值传递
。
即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响。
- 形参是基本数据类型:将实参基本数据类型变量的“
数据值
”传递给形参 - 形参是引用数据类型:将实参引用数据类型变量的“
地址值
”传递给形参
3.6 递归(recursion)方法
- 递归的分类:
- 直接递归
- 方法调用自身方法
- 间接递归
- A调B,B调C,C调A
- 直接递归
说明:
- 递归方法包含了一种 隐式的循环
- 递归方法 会 重复执行 某段代码,但这种重复执行无须循环控制。
- 递归一定要向 明确已知方向 递归,否则这种递归就变成了无穷递归,停不下来,类似于 死循环 。
最终 发生 栈内存溢出 。
- 递归需要明确 :
-
- 初始条件(入口)
-
- 结束条件(出口)
-
代码例子:计算1 ~ n的和
public class RecursionDemo {
public static void main(String[] args) {
RecursionDemo demo = new RecursionDemo();
//计算1~num的和,使用递归完成
int num = 5;
// 调用求和的方法
int sum = demo.getSum(num);
// 输出结果
System.out.println(sum);
}
/*
通过递归算法实现.
参数列表:int
返回值类型: int
*/
public int getSum(int num) {
/*
num为1时,方法返回1,
相当于是方法的出口,num总有是1的情况
*/
if(num == 1){
return 1;
}
/*
num不为1时,方法返回 num +(num-1)的累和
递归调用getSum方法
*/
return num + getSum(num-1);
}
}
- 递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环 慢的
多 ,所以在使用递归时要慎重。- 在要求高性能的情况下尽量避免使用递归,递归调用既花时间又 耗内存 。考虑使用循环迭
代
四、类的成员之一:构造(Constructor)
new
完对象时,所有成员变量
都是默认值,用构造器
可以为当前对象的某个或所有成员变量直接赋值。
没写构造,java 会提供一个默认的无参构造
构造器的修饰符只能是权限修饰符,不能被其他任何修饰。
比如,不能被static、final、synchronized、abstract、native修饰,不能有return语句返回值。
五、类的成员之一:代码块
如果成员变量想要初始化的值不是一个硬编码的常量值,而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值,该怎么办呢?此时,可以考虑代码块(或初始化块)。
-
代码块(或初始化块)的
作用
:- 对Java类或对象进行初始化
-
代码块(或初始化块)的
分类
:-
一个类中代码块若有修饰符,则只能被static修饰,称为静态代码块(static block)
-
没有使用static修饰的,为非静态代码块。
-
5.1 静态代码块
静态变量初始化,可以直接在静态变量的声明后面直接赋值,也可以使用静态代码块。
【修饰符】 class 类{
static{
静态代码块
}
}
- 可以对类的属性、类的声明进行初始化操作。
- 不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法。
- 静态代码块的执行要先于非静态代码块。
- 静态代码块随着类的加载而加载,且只执行一次。
- 若有多个静态的代码块,那么按照从上到下的顺序依次执行。
5.2 非静态代码块
和构造器一样,也是用于实例变量的初始化等操作。
- 可以对类的属性、类的声明进行初始化操作。
- 除了调用非静态的结构外,还可以调用静态的变量或方法。
- 每次创建对象的时候,都会执行一次。且先于构造器执行。
【修饰符】 class 类{
{
非静态代码块
}
【修饰符】 构造器名(){
// 实例初始化代码
}
【修饰符】 构造器名(参数列表){
// 实例初始化代码
}
}
如果多个重载的构造器有公共代码,并且这些代码都是先于构造器其他代码执行的,那么可以将这部分代码抽取到非静态代码块中,减少冗余代码。
实例变量赋值顺序:
六、OOP三大特性之一:封装
6.1 为什么需要封装
随着我们系统越来越复杂,类会越来越多,那么类之间的访问边界必须把握好,面向对象的开发原则要遵循“ 高内聚、低耦合 ”。
- 内聚,指一个模块内各个元素彼此结合的紧密程度;
- 耦合指一个软件结构内不同模块之间互连程度的度量。
- 内聚意味着重用和独立,耦合意味着多米诺效应牵一发动全身。
- 高内聚 :类的内部数据操作细节自己完成,不允许外部干涉;
- 低耦合 :仅暴露少量的方法给外部使用,尽量方便外部调用
把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。
6.2 如何实现封装
实现封装性 即 控制类或成员的可见性
具体修饰的结构:
外部类:public、缺省
成员变量、成员方法、构造器、成员内部类:public、protected、缺省、private
一般成员实例变量都习惯使用
private
修饰,再提供相应的public权限的get/set
方法访问。
对于final
的实例变量,不提供set()方法。
对于static final
的成员变量,习惯上使用public
修饰。
七、OOP三大特性之一:继承
is-a 的关系 , 父类与子类 , JAVA是单继承,多实现
减少了代码冗余,提高了代码的复用性,更有利于功能的扩展。
extends
- 子类会继承父类所有的实例变量和实例方法
- 子类不能直接访问父类中 私有的(private) 的成员变量和方法
- 可通过继承的get/set方法进行访问
- 继承的关键字用的是
extends
,即子类不是父类的子集,而是对父类的“扩展” - 一个父类可以同时拥有多个子类
- Java只支持单继承,不支持多重继承
八、OOP三大特性之一:多态
多态的使用前提:① 类的继承关系 ② 方法的重写
8.1 对象的多态性
多态(Polymorphism) 在继承的前提条件下,实现多态。 即 “ 父类引用指向子类对象 ”
(父类类型:指子类继承的父类类型,或者实现的接口类型)
- 对象的多态:在Java中,子类的对象可以替代父类的对象使用。
- 所以,一个引用类型变量可能指向(引用)多种不同类型的对象
8.2 多态理解
- Java引用变量有两个类型:
编译时类型
和运行时类型
。 - 编译时类型由
声明
该变量时使用的类型决定,运行时类型由实际赋给该变量的对象
决定。 - 编译看左边;运行看右边。
- 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
- 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法)
“看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
8.3 多态引用与多态的表现形式
- 方法调用:
- 编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
- 运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;
- 方法的形参声明体现多态
- 形参是父类类型,实参是子类对象
- 方法返回值类型体现多态
- 返回值类型是父类类型,实际返回的是子类对象
形参实参:
public void main(){
show(new student());//实参传递子类
}
public void show(Person person){ // 形参接收父类
person.方法;
}
返回值:
public Person show(){ // 返回值设置父类
retrun new Student(); // 实际返回子类
}
8.4 为什么需要多态性(polymorphism)?
代码举例:
狗与猫:
public class Dog {
public void eat(){
System.out.println("狗啃骨头");
}
}
public class Cat {
public void eat(){
System.out.println("猫吃鱼仔");
}
}
人类:
public class Person {
private Dog dog;
//adopt:领养
public void adopt(Dog dog){
this.dog = dog;
}
//feed:喂食
public void feed(){
if(dog != null){
dog.eat();
}
}
/*
问题:
1、从养狗切换到养猫怎么办?
修改代码把Dog修改为养猫?
2、或者有的人养狗,有的人养猫怎么办?
3、要是还有更多其他宠物类型怎么办?
如果Java不支持多态,那么上面的问题将会非常麻烦,代码维护起来很难,扩展性很差。
*/
}
8.4 多态的好处和弊端
好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。
Student m = new Student();
m.school = "pku"; //合法,Student类有school成员变量
Person e = new Student();
e.school = "pku"; //非法,Person类没有school成员变量
// 属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。
开发中:
使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。
【开闭原则OCP】
- 对扩展开放,对修改关闭
- 通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能
8.5 向上转型与向下转型
向下转型:
使用父类变量接收了子类对象之后,我们就不能调用
子类拥有,而父类没有的方法了。
所以,想要调用子类特有的方法,必须做类型转换,使得编译通过
。
8.5.1 如何向上或向下转型
向上转型:自动完成
向下转型:(子类类型)父类变量,可能发生报错ClassCastException,需要instanceof关键字判断
public class ClassCastTest {
public static void main(String[] args) {
//没有类型转换
Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog
//向上转型
Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dog
pet.setNickname("小白");
pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
// pet.watchHouse();//不能调用父类没有的方法watchHouse
Dog d = (Dog) pet;
System.out.println("d.nickname = " + d.getNickname());
d.eat();//可以调用eat方法
d.watchHouse();//可以调用子类扩展的方法watchHouse
Cat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
//这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
}
}
8.5.1 instanceof关键字
为了避免ClassCastException的发生,使用 instanceof
关键字,给引用变量做类型的校验。
//检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A
说明:
- 只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
- 如果对象a属于类A的子类B,a instanceof A值也为true。
- 要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误。
public class TestInstanceof {
public static void main(String[] args) {
Pet[] pets = new Pet[2];
pets[0] = new Dog();//多态引用
pets[0].setNickname("小白");
pets[1] = new Cat();//多态引用
pets[1].setNickname("雪球");
for (int i = 0; i < pets.length; i++) {
pets[i].eat();
if(pets[i] instanceof Dog){
Dog dog = (Dog) pets[i];
dog.watchHouse();
}else if(pets[i] instanceof Cat){
Cat cat = (Cat) pets[i];
cat.catchMouse();
}
}
}
}
九、Object 类的使用
类 java.lang.Object
是类层次结构的根类,即所有其它类的父类。每个类都使用 Object
作为超类。
9.1 ==和equals的区别
= =:
- 基本类型比较值 : 只要两个变量的值相等,即为true。
- 引用类型比较引用地址(是否指向同一个对象):只有指向同一个对象时,==才返回true。
- 用“==”进行比较时,符号两边的
数据类型必须兼容
(可自动转换的基本数据类型除外),否则编译出错
equals(): 所有类都继承了Object,也就获得了equals()方法。还可以重写。
- 只能比较引用类型,Object类源码中equals()的作用与“==”相同:比较是否指向同一个对象。
可以重写equals() 让它比较对象里面具体的属性,比如比较两者的姓名/年龄是否相同,来判断是不是同一个对象。
- == 可以比较基本数据类型和引用数据类型,基本数据类型比较值,引用数据类型比较地址!
- equals属于java.lang.Object类里面的方法,在没有重写的情况下,默认是和==用法一样。
- 具体要看自定义类里有没有重写Object的equals方法来判断。
- 通常情况下,重写equals方法,会比较类中的相应属性是否都相等。
9.2 toString()
public String toString()
① 默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式"
② 在进行String与其它类型数据的连接操作时,自动调用toString()方法
Date now=new Date();
System.out.println(“now=”+now); //相当于
System.out.println(“now=”+now.toString());
③如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()
因为Java的引用数据类型的变量中存储的实际上是对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM帮你调用了对象的toString()。
④ 可以根据需要在用户自定义类型中重写toString()方法
如String 类重写了toString()方法,返回字符串的值。
s1="hello";
System.out.println(s1);//相当于System.out.println(s1.toString());
9.3 getClass()
public final Class<?> getClass()
:获取对象的运行时类型
因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法
public static void main(String[] args) {
Object obj = new Person();
System.out.println(obj.getClass());//运行时类型
}
结果 : class com.wake.java.Person
9.4 hashCode()
public int hashCode()
:返回每个对象的hash值
public static void main(String[] args) {
System.out.println("AA".hashCode());//2080
System.out.println("BB".hashCode());//2112
}
十、关键字:
10.1 this
- this可以调用的结构:成员变量、方法和构造器
- this来区分
成员变量
和局部变量
- 同一个类中构造器互相调用
- this():调用本类的无参构造器
- this(实参列表):调用本类的有参构造器
10.2 super
使用super来调用父类中的指定操作:
- super可用于访问父类中定义的属性
- super可用于调用父类中定义的成员方法
- super可用于在子类构造器中调用父类的构造器
注意:
- 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
- super的追溯不仅限于直接父类
- super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识
10.2.1 super 使用场景
- 子类中调用父类被重写的方法
super.
能调用父类被重写的方法。而不使用子类中已经重写的父方法。
- 子类中调用父类中同名的成员变量
- 应该避免子类声明和父类重名的成员变量
- 子类构造器中调用父类构造器
- 子类继承父类时,不会继承父类的构造器。只能通过“
super(形参列表)
”的方式调用父类指定的构造器。
- 子类继承父类时,不会继承父类的构造器。只能通过“
开发中常见错误:
如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参的构造器,则编译出错
。
10.3 this与super
this:当前对象
- 在构造器和非静态代码块中,表示正在new的对象
- 在实例方法中,表示调用当前方法的对象
super:引用父类声明的成员
10.4 static
如果想让一个成员变量被类的所有实例所共享,就用static
修饰即可,称为类变量(或类属性)!
使用static
修饰,独属于类的,静态变量、静态方法。
-
使用范围:
- 在Java类中,可用static修饰属性、方法、代码块、内部类
-
被修饰后的成员具备以下特点:
- 随着类的加载而加载
- 优先于对象存在
- 修饰的成员,被所有对象所共享
- 访问权限允许时,可不创建对象,直接被类调用
10.5 final
final:最终的,不可更改的
- final修饰类
- 表示这个类不能被继承,没有子类
- final修饰方法
- 表示这个方法不能被子类重写。
- final修饰变量
- final修饰某个变量(成员变量或局部变量),一旦赋值,它的值就不能被修改,
- 即常量,常量名建议使用大写字母。
如果某个成员变量用final修饰后,是没有set方法的,并且必须初始化(可以显式赋值、或在初始化块赋值、实例变量还可以在构造器中赋值)
十一、抽象类与抽象方法(abstract关键字)
无法给出具体的实现,就只有方法签名,没有方法体。
- 抽象类:被abstract修饰的类。
- 抽象方法:被abstract修饰没有方法体的方法。
抽象类
[权限修饰符] abstract class 类名{
}
[权限修饰符] abstract class 类名 extends 父类{
}
抽象方法:
[其他修饰符] abstract 返回值类型 方法名([形参列表]);
- 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。
- 抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。
- 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
- 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。
- 不能用abstract修饰变量、代码块、构造器;
- 不能用abstract修饰私有方法、静态方法、final的方法、final的类。
十二、接口(interface)
- 接口实现是 "能不能"的
has-a
关系,使用interface
关键字, - JAVA类多实现,单继承;
- 接口自身可以多继承(也使用
extends
关键字) - 也会被编译成.class文件,
- 但是它并不是类,而是另外一种引用数据类型。
引用数据类型:数组,类,枚举,接口,注解。
[修饰符] interface 接口名{
//接口的成员列表:
// 公共的静态常量
// 公共的抽象方法
// 公共的默认方法(JDK1.8以上)
// 公共的静态方法(JDK1.8以上)
// 私有方法(JDK1.9以上)
}
public interface USB3{
//静态常量
long MAX_SPEED = 500*1024*1024;//500MB/s
//抽象方法
void in();
void out();
//默认方法
default void start(){
System.out.println("开始");
}
default void stop(){
System.out.println("结束");
}
//静态方法
static void show(){
System.out.println("USB 3.0可以同步全速地进行读写操作");
}
}
12.1 接口的使用
接口不能创建对象,但是可以被类实现(implements
,类似于被继承)。
所有父接口的抽象方法都有重写。
方法签名相同的抽象方法只需要实现一次。
【修饰符】 class 实现类 implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}
【修饰符】 class 实现类 extends 父类 implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}
注意:
- 如果接口的实现类是非抽象类,那么必须
重写接口中所有抽象方法
。- 默认方法可以选择保留,也可以重写。
- 接口中的静态方法不能被继承也不能被重写
- 接口与实现类对象构成多态引用
- 接口引用指向接口的实现类
- 使用接口的静态成员 (JDK8.0才能开始使用)
- 通过“
接口名.
”调用接口的静态方法 (JDK8.0才能开始使用) - 通过“
接口名.
”直接使用接口的静态常量
- 通过“
- 使用接口的非静态方法
- 直接使用“
接口名.
”进行调用 - 接口的抽象方法、默认方法,只能通过实现类对象才可以调用
- 直接使用“
12.2 JDK8中相关冲突问题
12.2.1 默认方法冲突问题
(1)类优先原则
当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的抽象方法重名,子类就近选择执行父类的成员方法。
(2)接口冲突
- 当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时。
- 选择保留其中一个,通过“
接口名.super.方法名
"的方法选择保留哪个接口的默认方法。
- 选择保留其中一个,通过“
public class Girl implements Friend,BoyFriend{
@Override
public void date() {
//(1)保留其中一个父接口的
// Friend.super.date();
// BoyFriend.super.date();
//(2)完全重写
System.out.println("完全重写方法");
}
}
- 当一个子接口同时继承了多个接口,而多个父接口中包含方法签名相同的默认方法时
- 只会继承 重写默认方法
public interface USB extends USB2,USB3 {
@Override
default void start() {
System.out.println("Usb.start");
}
@Override
default void stop() {
System.out.println("Usb.stop");
}
}
子接口重写默认方法时,default关键字可以保留。
子类重写默认方法时,default关键字不可以保留。
12.2.2 常量冲突问题
- 当子类继承父类又实现父接口,而父类中存在与父接口常量同名的成员变量,并且该成员变量名在子类中仍然可见。
- 当子类同时实现多个接口,而多个接口存在相同同名常量。
通过接口名.
指定常量
继承使用super.
常量
12.3 接口的总结与面试题
- 接口本身不能创建对象,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用。
- 声明接口用interface,接口的成员声明有限制:
- (1)公共的静态常量
- (2)公共的抽象方法
- (3)公共的默认方法(JDK8.0 及以上)
- (4)公共的静态方法(JDK8.0 及以上)
- (5)私有方法(JDK9.0 及以上)
- 类可以实现接口,关键字是implements,而且支持多实现。如果实现类不是抽象类,就必须实现接口中所有的抽象方法。如果实现类既要继承父类又要实现父接口,那么继承(extends)在前,实现(implements)在后。
- 接口可以继承接口,关键字是extends,而且支持多继承。
- 接口的默认方法可以选择重写或不重写。如果有冲突问题,另行处理。子类重写父接口的默认方法,要去掉default,子接口重写父接口的默认方法,不要去掉default。
面试题
1、为什么接口中只能声明公共的静态的常量?
因为接口是标准规范,那么在规范中需要声明一些底线边界值,当实现者在实现这些规范时,不能去随意修改和触碰这些底线,否则就有“危险”。
例如:USB1.0规范中规定最大传输速率是1.5Mbps,最大输出电流是5V/500mA
USB3.0规范中规定最大传输速率是5Gbps(500MB/s),最大输出电流是5V/900mA
例如:尚硅谷学生行为规范中规定学员,早上8:25之前进班,晚上21:30之后离开等等。
2、为什么JDK8.0 之后允许接口定义静态方法和默认方法呢?因为它违反了接口作为一个抽象标准定义的概念。
静态方法
:因为之前的标准类库设计中,有很多Collection/Colletions或者Path/Paths这样成对的接口和类,后面的类中都是静态方法,而这些静态方法都是为前面的接口服务的,那么这样设计一对API,不如把静态方法直接定义到接口中使用和维护更方便。
默认方法
:
(1)我们要在已有的老版接口中提供新方法时,如果添加抽象方法,就会涉及到原来使用这些接口的类就会有问题,那么为了保持与旧版本代码的兼容性,只能允许在接口中定义默认方法实现。比如:Java8中对Collection、List、Comparator等接口提供了丰富的默认方法。
(2)当我们接口的某个抽象方法,在很多实现类中的实现代码是一样的,此时将这个抽象方法设计为默认方法更为合适,那么实现类就可以选择重写,也可以选择不重写。
3、为什么JDK1.9要允许接口定义私有方法呢?因为我们说接口是规范,规范是需要公开让大家遵守的。
私有方法:因为有了默认方法和静态方法这样具有具体实现的方法,那么就可能出现多个方法由共同的代码可以抽取,而这些共同的代码抽取出来的方法又只希望在接口内部使用,所以就增加了私有方法。
12.4 接口与抽象类之间的对比
在开发中,常看到一个类不是去继承一个已经实现好的类,而是要么继承抽象类,要么实现接口。
十三、内部类(InnerClass)
将一个类A定义在另一个类B里面,里面的那个类A就称为内部类(InnerClass)
,类B则称为外部类(OuterClass)
。
13.1 为什么要声明内部类
当一个事物A的内部,还有一个部分需要一个完整的结构B进行描述,而这个内部的完整的结构B又只为外部事物A提供服务,不在其他地方单独使用,那么整个内部的完整结构B最好使用内部类。
总的来说,遵循高内聚、低耦合
的面向对象开发原则。
13.2 内部类的分类
十四、枚举类 enum
枚举类型本质上也是一种类,只不过是这个类的对象是有限的、固定的几个。若枚举只有一个对象, 则可以作为一种单例模式的实现方式。
// 定义
public enum Week {
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
}
// 使用
public static void main(String[] args) {
Season spring = Season.SPRING;
System.out.println(spring);
}
14.1 enum中常用方法
String toString(): 默认返回的是常量名(对象名),可以继续手动重写该方法!
static 枚举类型[] values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值,是一个静态方法
static 枚举类型 valueOf(String name):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。
String name():得到当前枚举常量的名称。建议优先使用toString()。
int ordinal():返回当前枚举常量的次序号,默认从0开始
十五、包装类
15.1 为什么需要包装类
基本数据类型要使用为空null 情况
15.2 有哪些包装类
内存变化:
15.3 包装类与基本数据类型间的转换
15.3.1 装箱
装箱:把基本数据类型转为包装类对象
基本数值---->包装对象
Integer obj1 = new Integer(4);//使用构造函数函数
Float f = new Float(“4.56”);
Long l = new Long(“asdf”); //NumberFormatException
Integer obj2 = Integer.valueOf(4);//使用包装类中的valueOf方法
15.3.2 拆箱
拆箱:把包装类对象拆为基本数据类型
包装对象---->基本数值
Integer obj = new Integer(4);
int num1 = obj.intValue();
自动装箱与拆箱: (JDK5后)
Integer i = 4;//自动装箱。相当于Integer i = Integer.valueOf(4);
i = i + 5;//等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5;
//加法运算完成后,再次装箱,把基本数值转成对象。
注意:只能与自己对应的类型之间才能实现自动装箱与拆箱。
Integer i = 1;
Double d = 1;//错误的,1是int类型
15.4 基本数据类型、包装类与字符串间的转换
① 基本数据类型转为字符串
方式1: 调用字符串重载的valueOf()方法
int a = 10;
//String str = a;//错误的
String str = String.valueOf(a);
方式2 :直接 +""
int a = 10;
String str = a + "";
② 字符串转为基本数据类型
方式一:
public static int parseInt(String s)
:将字符串参数转换为对应的int基本类型。public static long parseLong(String s)
:将字符串参数转换为对应的long基本类型。public static double parseDouble(String s)
:将字符串参数转换为对应的double基本类型。
方式二:
字符串转为包装类,然后可以自动拆箱为基本数据类型
public static Integer valueOf(String s)
:将字符串参数转换为对应的Integer包装类,然后可以自动拆箱为int基本类型public static Long valueOf(String s)
:将字符串参数转换为对应的Long包装类,然后可以自动拆箱为long基本类型public static Double valueOf(String s)
:将字符串参数转换为对应的Double包装类,然后可以自动拆箱为double基本类型
注意:如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出
java.lang.NumberFormatException
异常。
方式三:
通过包装类的构造器实现
int a = Integer.parseInt("整数的字符串");
double d = Double.parseDouble("小数的字符串");
boolean b = Boolean.parseBoolean("true或false");
int a = Integer.valueOf("整数的字符串");
double d = Double.valueOf("小数的字符串");
boolean b = Boolean.valueOf("true或false");
int i = new Integer(“12”);
15.5 包装类的其它API
数据类型的最大最小值
Integer.MAX_VALUE和Integer.MIN_VALUE
Long.MAX_VALUE和Long.MIN_VALUE
Double.MAX_VALUE和Double.MIN_VALUE
字符转大小写
Character.toUpperCase('x');
Character.toLowerCase('X');
比较的方法
Double.compare(double d1, double d2)
Integer.compare(int x, int y)
整数转进制
Integer.toBinaryString(int i)
Integer.toHexString(int i)
Integer.toOctalString(int i)
15.5 包装类对象的特点
Integer a = 1;
Integer b = 1;
System.out.println(a == b);//true
Integer i = 128;
Integer j = 128;
System.out.println(i == j);//false
Integer m = new Integer(1);//新new的在堆中
Integer n = 1;//这个用的是缓冲的常量对象,在方法区
System.out.println(m == n);//false
Integer x = new Integer(1);//新new的在堆中
Integer y = new Integer(1);//另一个新new的在堆中
System.out.println(x == y);//false
Double d1 = 1.0;
Double d2 = 1.0;
System.out.println(d1==d2);//false 比较地址,没有缓存对象,每一个都是新new的
15.5.1 类型转换问题
Integer i = 1000;
double j = 1000;
System.out.println(i==j);//true 会先将i自动拆箱为int,然后根据基本数据类型“自动类型转换”规则,转为double比较
Integer i = 1000;
int j = 1000;
System.out.println(i==j);//true 会自动拆箱,按照基本数据类型进行比较
Integer i = 1;
Double d = 1.0
System.out.println(i==d);//编译报错
15.5.2 包装类对象不可变
public class TestExam {
public static void main(String[] args) {
int i = 1;
Integer j = new Integer(2);
Circle c = new Circle();
change(i,j,c);
System.out.println("i = " + i);//1
System.out.println("j = " + j);//2
System.out.println("c.radius = " + c.radius);//10.0
}
/*
* 方法的参数传递机制:
* (1)基本数据类型:形参的修改完全不影响实参
* (2)引用数据类型:通过形参修改对象的属性值,会影响实参的属性值
* 这类Integer等包装类对象是“不可变”对象,即一旦修改,就是新对象,和实参就无关了
*/
public static void change(int a ,Integer b,Circle c ){
a += 10;
// b += 10;//等价于 b = new Integer(b+10);
c.radius += 10;
/*c = new Circle();
c.radius+=10;*/
}
}
class Circle{
double radius;
}
十六、注解(Annotation)
@注解名
@author 标明开发该类模块的作者,多个作者之间使用,分割
@version 标明该类模块的版本
@see 参考转向,也就是相关主题
@since 从哪个版本开始增加的
@param 对方法中某参数的说明,如果没有参数就不能写
@return 对方法返回值的说明,如果方法的返回值类型是void就不能写
@exception 对方法可能抛出的异常进行说明 ,如果方法没有用throws显式抛出的异常就不能写
框架 = 注解 + 反射 + 设计模式
16.1 JUnit单元测试
16.1.1 测试分类
**黑盒测试:**不需要写代码,给输入值,看程序是否能够输出期望的值。
**白盒测试:**需要写代码的。关注程序具体的执行流程。
JUnit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。
要使用JUnit,必须在项目的编译路径中
引入JUnit的库
jar包
16.1.2 编写和运行@Test单元测试方法
@Test标记方法
@Test
public void test01(){
System.out.println("TestJUnit.test01");
}
16.1.3 设置执行JUnit用例时支持控制台输入
默认情况下,在单元测试方法中使用Scanner时,并不能实现控制台数据的输入。需要做如下设置:
在idea64.exe.vmoptions配置文件
中加入下面一行设置,重启idea后生效。
-Deditable.java.test.console=true
16.1.4 定义test测试方法模板
十七、单例(Singleton)设计模式
17.1 设计模式概述
设计模式是在大量的实践中总结
和理论化
之后优选的代码结构、编程风格、以及解决问题的思考方式。
经典的设计模式共有23种:
17.2 何为单例模式
对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
17.3 单例模式的两种实现方式
17.3.1 饿汉式
class Singleton {
// 1.私有化构造器
private Singleton() {
}
// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single = new Singleton();
// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
return single;
}
}
17.3.1 懒汉式
class Singleton {
// 1.私有化构造器
private Singleton() {
}
// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single;
// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
if(single == null) {
single = new Singleton();
}
return single;
}
}
17.4 饿汉式 vs 懒汉式
饿汉式:
- 特点:
立即加载
,即在使用类的时候已经将对象创建完毕。 - 优点:实现起来
简单
;没有多线程安全问题。 - 缺点:当类被加载的时候,会初始化static的实例,静态变量被创建并分配内存空间,从这以后,这个static的实例便一直占着这块内存,直到类被卸载时,静态变量被摧毁,并释放所占有的内存。因此在某些特定条件下会
耗费内存
。
懒汉式:
- 特点:
延迟加载
,即在调用静态方法时实例才被创建。 - 优点:实现起来比较简单;当类被加载的时候,static的实例未被创建并分配内存空间,当静态方法第一次被调用时,初始化实例变量,并分配内存,因此在某些特定条件下会
节约内存
。 - 缺点:在多线程环境中,这种实现方法是完全错误的,
线程不安全
,根本不能保证单例的唯一性。- 说明:在多线程章节,会将懒汉式改造成线程安全的模式。
17.5 单例模式的优点及应用场景
由于单例模式只生成一个实例,减少了系统性能开销
,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
应用场景:
-
Windows的Task Manager (任务管理器)就是很典型的单例模式
-
Windows的Recycle Bin (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
-
Application 也是单例的典型应用
-
应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
-
数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
总结
-
在类的属性中,可以有哪些位置给属性赋值?
① 默认初始化
② 显式初始化
③ 构造器中初始化
④ 通过"对象.属性"或"对象.方法"的方式,给属性赋值 -
JavaBean 实体类
- 类是公共的
- 有一个无参的公共的构造器
- 有属性,且有对应的get、set方法
-
UML类图
UML
(Unified Modeling Language,统一建模语言),用来描述软件模型和架构的图形化语言。
UML类图
可以更加直观地描述类内部结构(类的属性和操作)以及类之间的关系(如关联、依赖、聚合等)。
- +表示 public 类型, - 表示 private 类型,#表示protected类型
- 方法的写法: 方法的类型(+、-) 方法名(参数名: 参数类型):返回值类型
- 斜体表示抽象方法或类。
- JDK中主要的包介绍
- java.lang ----包含一些Java语言的核心类,如String、Math、Integer、 System和Thread,提供常用功能
- java.net ----包含执行与网络相关的操作的类和接口。
- java.io ----包含能提供多种输入/输出功能的
类。 - java.util ----包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关
的函数。 - java.text ----包含了一些java格式化相关的类
- java.sql ----包含了java进行JDBC数据库编程
的相关类/接口 - java.awt ----包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。