面向对象基础
面向对象是一种编程思想,其本身与编程语言无关。编程思想是我们在编写程序、分析问题域时采用的一种思维方式。
OOA ---- 面向对象分析、OOD ---- 面向对象设计、OOP ---- 面向对象编程、OOT ---- 面向对象测试
结构化编程与面向对象编程的区别
结构化编程,程序是围绕要解决的任务来设计。面对对象编程,程序是围绕着问题域中的对象来设计。
面向对象与Java
Java 只是在它的语法设计上直接引入了面面向对象的概念,在语句层面上能够表现面对对象的各种特征。
C++是第一门在语法中引入OO思想的语言,但是由于它当时的设计还需要兼顾C的特性,导致它不能算为纯粹的面向对象编程语言。Java是第一门纯面向对象的编程语言,它的整个设计完全是基于面向对象中所需要用到的概念。
面向对象的设计方案核心:
- 从问题域场景中“找对象”,找出这些对象有哪些属性、哪些行为。
属性 ----对象身上的值数据。名词
行为 ----对象身上的功能。动词或动宾
结论:万物皆对象、对象因关注而产生;
- 在一个问题域中我们会找到大量的同类型对象,会根据类型进行编码,这就是“类”的概念
对象 ---- 是具体的实际的存在,是我们真正去操作的东西。
类 ------- 是通过人脑的抽取,把一系列具有相同属性和行为的对象进行归纳,形成的定义。
关键点:
-
类是一种数据类型,这种数据类型可以装很多数据值(姓名、年龄、身高、体重....),同时还可以定义他们能够做什么事情( 行为:上课、跑步、吃饭、休息....)
-
定义好的类,是不能直接使用的。类是用来产生对象的,具体要用是用对象。类相当于是一个“模板”,通过 这个模板产生具有相同属性和相同行为的对象。
注意:
- 相同属性指的是都有这个属性,但是值可以不一样。2. 类是对象的抽取,对象是类的实例。
面向对象的情况下,我们分析和编码的方式步骤:
从整个问题域当中找对象,包括对象的属性和行为-----> 编码的时候,先定义类------->通过类来产生对象,最后使用对象来产生效果。
类
定义类的语法
//创建一个Java文件
public class 类名{
//属性
public 类型 属性1;
public 类型 属性2;
public final 类型 属性3 = 值;
//行为
public 返回类型 方法名(形参列表){
实现指令
}
}
-
属性 ----这个类型身上所具有的数据,本质上是数据量
-
如果这个类的所有对象在该属性上的值是不同的,是可以变化的,那么就声明为变量;
-
如果该类所有对象在这个属性上的值是一样的,是不可变化的,那么就声明为常量
在实际使用中,变量属性占主导地位,常量属性很少
语法特点
-
属性的语法本质上就是变量或常量的语法;
-
属性所书写的位置,是直接被放在类的
{}
内。这种放的位置,导致了属性可以在整个类的任意方法当中使用 --- 又被称之为“全局变量”,而放在方法里面的变量,只能在这个方法里面用 --- “局部变量” -
属性是可以有修饰符的,局部变量没有。
public
公共的、protected
受保护的、private
私有的 ----总共3个关键字4种情况,这些访问修饰符是用来控制这些属性能不能在外部被直接访问到 -
属性在定义的时候可以不赋初值,它会自动进行初始化;
基本数据类型的属性赋初值为0;引用数据类型赋初值为null;局部变量在使用前必须赋初值
-
-
行为 ---- 行为是功能模块,是这个类的对象能够做什么事情,所以它的语法就是在定义方法
与之前在上个阶段的方法声明相比,少一个
static
关键字.static
关键字是用来控制这个方法是不是静态的,静态的方法是不能被类的对象调用的,由它声明描述的方法在Java的面向对象概念中代表特殊含义,普通的对象行为是没有这个关键字的
通过类产生对象
new
关键字产生对象
语法:
new 类名()
-
类名 对象名 --- 这句代码不是产生对象,而只产生一个存对象引用的变量空间
-
类名 对象名 = new 类名() ---表示这个对象名里面的引用指向new出来的这个对象。说明:类类型是引用数据类型
对象创建好了以后,我们才能够操作它的属性(赋值或取值),或者调用它的方法,否则都会发生做空指针异常 -- NullPointerException
//产生1个学生对象
/*
* Student stu -- 声明了一个学生变量,但是并没有产生学生对象
* 产生学生对象的语法:new 类名()
* 然后通过"=",把学生对象放到stu0变量中。
* stu0 不是对象,而是一个Student类型的引用变量,这个变量不是用来存放对象的,而是用来存放一个引用指向真正的学生对象。
* 真正的学生对象是new语句产生的,它是放在一个独立的内存空间中,学生所有属性数据是放在对象内部的。
*/
Student stu0 = new Student();
Student stu1 = new Student();
stu0.stuName = "张三";
stu0.study();//由于study是没有static修饰的,所以需要通过对象名的引用去调用
stu1.stuName = "李四";
stu1.age = 20;
注意:
-
在通过变量访问调用对象的属性或方法前,必须先判断对象是否为空,如果为空,则抛出空指针异常。
-
对象变量是有数据类型的,比如stu0,我们在声明的时候就使用了Student类型去修饰,所以它只能赋值为Student类型的对象,否则编译不通过。
-
目前不用强转的方式去进行转换,因为在编译的时候已经确定了类型,所以不需要强转。
-
对象变量的赋值只有两种:
该变量对应的对象;null赋值,如:stu0 = null;
操作类的语法
点操作符,对象名通过点操作符来访问它指向的对象的内部信息,包括该对象的属性或方法。
销毁对象
用new
关键字产生了对象,但对象产生在内存中的具体位置,我们是不知道的,所以我们必须用一个引用变量指向它。然后通过这个引用变量来使用它。
假如有一个对象是没有引用指向的(赋值为null),那么在程序的后续代码中无法找到和使用它。该对象就无意义,这个时候这个对象就会被销毁掉。
对象的销毁在Java当中是由JVM(java虚拟机)自己来做。它有一个非常著名的机制 --- 垃圾回收机制(GC),这个机制会随时检查内存,一旦发现某个对象没有引用指向了,那么这个对象就会纳入到它的回收范围中。
这个垃圾回收是自动运行的,它不受程序员的控制。when && how都由它内部算法决定,只需要信任即可。
程序员在代码级别对垃圾回收的控制,只有一条指令:System.gc();
这条指令的任务就是召唤垃圾回收机制,让它来清理一次。when && how还是由GC内部算法决定。
tips:
- 销毁对象不是由程序员来完成,而是由GC来完成;
- 没有引用指向的对象会被视为垃圾;
- GC什么时候回收,回收哪些没有引用指向的对象,都由它自身算法决定;
System.gc()
方法是用来召唤GC,但是什么时候收,如何收,仍然由GC自己决定。
对象的关联关系
对象的三种关联关系
as-a 关系
有一个,指的是一个对象的内部“拥有”另一个对象。
在语法上,has-a关系就是把一个关系的属性设计为另一个对象的属性。关联分 单向关联 和 双向关联
- A has B 或B has A都叫单向关联
- A has B 且B has A 叫做双向关联
把对方设计为自己的属性后,在当前对象的所有方法中,都可以访问到该对象。换句话说,两个对象的生命周期是保持一致的.
use-a关系
用一个,指的是在一个对象的某个方法中“使用”另一个对象。
在语法上,use-a关系是把一个对象设计为另一个方法的局部变量。
该变量可以通过参数传递进这个方法,也可以自己在方法内部new一个,两者无论哪一个,都是use-a关系,因为参数也好,自己方法内部声明定义也好,都是方法的局部变量,方法结束,局部变量消失,关系解除。
is-a关系
是一个,指的是一个对象是另外一种对象的特例。
this关键字
在类的内部方法中,访问当前类的属性或调用自己的某个方法,也有一个默认的对象--用this
表示,这个this
代表的是”当前对象“,相当于”我“的含义,谁调用这个方法谁就是”当前对象“。
利用this.
,后面可以访问自己属性和自己的行为。书写this.
的好处:
-
利用IDEA工具,书写上
this.
以后,可以快速联想出本类的属性和方法,提高编程速度 -
可以区分局部变量和属性重名的情况。
对于this.
的使用,我们要分情况
-
如果出现局部变量和全局变量重名,我们又需要特指全局的时候,那么就一定要用this.
-
如果没出现上面的情况,那就视程序员的习惯和公司要求的代码规范
包
包的命名规范:全小写;包在本质上就是专门用来存放java类的文件夹。当一个java类被放入到某个包当中的时候有以下几个特点:
-
在这个类的最高处要增加一句包申明的代码,使用关键字
package
,后面跟上包的名字。比如:
package com.lovo.test
; -
在默认情况下,一个java类只能访问来自于同一个包的其他java类。如果需要访问其他包的类,那么需要使用关键字“
import
”,把那个类导入进来。 比如:import com.lovo.practise.Car
; 可以使用import com.lovo.practise.*
; 导入practise
包下面所有的java类,但是不建议这么做,最好的方式是需要用谁才导入谁。 -
在IDEA工具中,具有多种创建包或创建类的方式,需要大家逐步的熟悉。
对象的行为
-
在对象的方法定义的时候,方法的声明部分(方法签名),不要写
static
; -
方法的调用仍然满足我们之前所学习的“方法调用栈”的特性,先进后出。这里的栈指的是方法调用和结束的顺序是满足数据结构中“栈”的概念 --- 先进后出。
-
方法结束的方式:
-
正常结束 -- 不管带不带返回值,只要是这个方法正常执行,直到遇到方法的结束"}" 或 “return” 语句,都是正常结束的。
-
异常结束 -- 方法在执行过程中发生了问题,执行不下去了,被强制结束。
但不管是正常结束还是异常结束,方法的流程都是返回到方法调用处。
-
-
访问修饰符 返回类型 方法名(参数列表)
- 访问修饰 --- 肯定要有,不写也有默认,默认不是public,是一种独立情况
- 返回类型 --- 可以返回任意数据类型: 基本数据类型、String、自定义类型、数组类型 均可。 返回也是返回“值”,只不过基本数据类型的值就是数据值本身,而引用数据类型的值就是它的引用。如果没有返回,要写
void
- 参数列表 -- 指的是形参列表,语法就是变量声明的语法
-
重载方法 -- 一个类可以拥有多个同名的方法,要求是参数列表不同,包括:参数个数、参数的类型、参数的顺序,跟参数名无关。
构造器--构造方法--Constructor
-
构造方法的作用:
-
产生对象;
-
通过接受外部参数,对对象的属性进行初始化
-
构造方法的语法特殊性:
- 程序员可以在类当中不定义构造方法, 那么Java会给我们自动生成一个“公共的无参的”构造方法,保证我们的基本使用;
- 如果我们要自定义构造方法,那么需要注意
- 构造方法的名字必须和类名保持一致;
- 构造方法没有返回类型,连void都没有;
- 构造方法的访问修饰符和参数列表可以根据具体问题域具体设计。
- 一旦自定义了构造方法,那么Java将不再给我们生成那个默认的“公共无参”构造;
-
使用语法
类名 对象名 = new 构造方法(实参列表)
-
构造方法的执行步骤:
- 在内存的堆当中划分了对象存放的空间,这个空间多大呢?依赖于我们给这个类定义了多少内容
- 在该空间划分属性,并对属性进行初始化;
- 执行程序员在构造方法当中所书写的代码。
其中,头两件事情是构造方法自己做的,我们无需用代码表达,我们只需要书写第三个步骤的内容即可。
相关知识点 -- this()
this()
-- 用来让一个构造方法可以共享另一个构造方法的代码
语法特殊性:
this()
只能写在构造方法里面;this()
只能写在构造方法的第一句;这也导致一个构造只能共享一次其他构造的代码;- 如果一个类里面有多个构造方法,this()是依赖于它的参数去匹配到底调用的是哪个构造方法;
- 构造方法不允许递归调用,不能用
this()
访问自己。
与面试相关的this()
问题
1. 如何在外部new 一次对象,然后调用到某个类的所有构造方法?
2. 构造方法的作用是用来产生对象的,调用一次构造就产生一个对象,那么如果构造方法里面又写了this(),那是否意味着new 一次 多次调用了构造方法,从而产生了多个对象呢?
3. this有几种用法?
4. 分辨this和super关键字。--- 放到后面来讲
属性的访问器和修改器 --getter/setter
其作用就是为了能够让外部使用我们在类当中被设计为
private
的这些属性。
- 访问器(getter)语法:
public 被访问的属性的类型 get被访问的属性名(){
return 被访问的属性;
}
- 修改器(setter)语法:
public void set被访问的属性名(属性类型 属性名){
this.属性名 = 属性名;
}
优点:
我们能够通过为某个private属性 单独提供get方法 或 set方法,让这个属性对外部而言是“只读”的,或“只写”的;这是把属性设计为public做不到的。
如果我们直接把属性设置为public,外部可以对这个属性赋任意数据值(只要满足类型匹配,但不能考虑有效性匹配),除非外部每一次赋值的时候都去做判断;而如果设计为private属性,然后提供set方法,那么外部只能通过set方法给该属性赋值,而我们可以在set内部通过代码进行有效性验证。这样把验证有效数据的控制权掌握在一个地方,而不是散在每一次赋值的地方。
不直接暴露我们类内部对数据的存放形式,比如 学生类身上的生日属性,在类内部其实设计为年月日三个int,但由于是私有的,外部看不到,只能看到我统一提供的一个叫做getBirthday的方法,获取最终结果。
java 标准类
Java标准类,又名“Java Bean”,也被叫做“Bean类”。 JavaBean类 是 SUN公司在95年的“技术白皮书”当中提出,只有满足这些规范的类才能称之为“JavaBean”。Java是一门工程学的编程语言,它最大的特点就是建立庞大的项目,所以标准是很重要的。到了后期,你们学习Java方向的同学会发现,所有的框架/工具,几乎都要求书写Java标准类,不满足要求的根本不能在这些框架和工具当中运行起来。
Java标准类的规范如下:
-
必须提供公共无参构造;
-
必须为私有属性提供符合命名规范的get/set方法;
-
应该实现Serializable接口
一定要习惯get/set方法的使用
面向对象四大特征--封装、继承
面向对象在问题域中的分析与设计(简单版)
-
在问题域当中寻找名词
- 一个是在Java语言当中,没有现成数据类型表示的,那么就是需要我们去自定义的类(类型);
- 一个是可以在Java语言当中找到数据类型来表示的名词,那么它们通常都应该是某个我们自定义类的属性
-
把找到的属性归纳到它所属的类当中去
-
分析类与类之间(对象与对象之间)的关联关系
-
分析动词,动词就是行为,然后关注这些行为是设计在哪个类当中
-
现在在简单问题域当中,我们已经可以用代码表示出基本类结构
如果是在复杂问题域(比如以后你们要做的大一点的项目),那么我们就可以使用设计工具进行进一步的图形化设计了(比如:类图、流程图等等)。
在定义代码的基本结构的时候,类的定义和属性的定义后好办,关键是方法的定义(特别是方法的返回和参数)。在定义方法的返回和参数的时候一定要确认方法在哪里被调用。
-
设计结束以后,然后再挨个开发每一个功能模块。切记不要一次性实现完,而是实现一个测试一个,保证每个模块的正确性。
面向对象和面向过程的对比
面向对象最大的特点就是分析的过程不再采用解决问题的步骤作为分析的切入点,而是换成分析问题域中参与的角色作为切入点。 它主要针对的是大型问题域,由于过程复杂,数据的交互比较繁琐,所以必须有这么一个分析过程,去整理清楚角色、角色职责和角色的关系。
常见面试问题:
面向过程和面向对象谁好?或者说谁更符合人类的自然思维? 在我的观点来看,这个问题是一个比较性的问题,而不是得出谁比谁好,应该是在某个问题域当中谁比谁更自然一点。
- 两者都是符合人类自然思维的;只不过这个自然性是表现在不同情况下,当一个人解决他熟悉或他能够控制的问题域的时候,他的自然思维必然是面向过程的;但是当一个人遇到他不熟悉的问题域,那么他是不可能整理出步骤的,所以他只有先去认识整个问题域。而认识整个问题域的方式呢?最自然的就是分部件(分工)去认识里面每一个参与角色。
- 面向对象不可能替换掉面向过程;面向对象更大的优势在于分析陌生问题域,然后搭建开发人员的认知结构和代码结构,但是一旦到了具体实现的时候,还是需要一定过程性的思维,至少每个方法的实现还是有过程性的表现呀。
- 以后的学习过程中,我们还会遇到其他的面向(比如:面向接口,面向切面等等),它们其实是在特殊问题域中站在另一个角度去分析看待问题域。
对象的封装Encapsulation
我们在前面学习到的类的定义,访问修饰符这些都属于面向对象当中“封装”这个特性。
在我们前面学习的“类”的定义,其本质就是在学习封装当中“装”的这个过程。我们把我们在系统当中所需要用到的数据和功能进行分门别类的放置,有些设计在A类,有些在B类当中。我们可以认为“类定义”当中的那对“{}
”其实就是我们装的边界。
包的引入其实也是一个“装”的体现。一个包里面的类理论上只能直接认识本包中的其他类,来自于其他包的类必须使用 “import
告知”。我们在写一个类的时候,不需要用import
来告知的是什么呢?
- 来自于
java.lang
包中JDK类;- 自定义于本包中的类。
- 有一种特殊情况,我们要在自定义类中使用多个需要import进来的同名类,需要在前面加上限定包名
遇到多个同类型的对象时把它们可以装到一个集合中去。这里,对象也可以放到数组中。
面试中,经常有两个词汇,“基本数据类型数组”和“引用数据类型数组”。大家要注意认知清楚:
- 数组一定是引用数据类型;
- “基本数据类型数组”是指数组中的元素是基本数据类型;“引用数据类型数组”是指数组中的元素是引用数据类型。
面试的时候遇到“封装”的问题,要注意:
- 封装不仅仅只是“封”,还包括“装”的方面;
- “封”也不仅仅是信息的隐藏(数据的隐藏),还包括方法的具体实现的隐藏。方法本身就是把实现细节进行“封”的实现,让外部的调用者看不到具体的实现(对于外部调用者来说用就可以了)。
封装在面向对象中体现的是“归纳”与“信息的隐藏”
类的高级概念
static
static关键字 -- 是一个可选修饰符,它本身的含义叫做“静态”。 它不仅只能修饰方法,它也可以修饰:属性、代码块、内部类 。 其中需要关注的是属性、方法 和 构造方法。 那么static可以修饰其中两个:属性和方法。不能修饰构造方法。另外今天还会给大家增加能够在类里面书写的两个内容:代码块(初始化块)以及内部类。
类里面总共可以书写的内容是5个:属性、构造、方法、初始化块和内部类。其中:前三个是需要重点掌握,也是最常操作的。初始化块在大型项目当中可能会在部分关键类里面出现,所以需要有一定的认知度。内部类需要了解。
static与 非static的属性
在一个类当中,属性也可以选择使用static进行修饰。
语法上的对比:
-
有static
- 有static修饰的属性可以用"类名."的方式直接访问,也可以通过“对象.”的方式进行访问(但第二种方式,不推荐)。
- 用static修饰的属性,是全类的所有对象共享一个值,无论是该类的哪个对象把这个值改了,那么大家的值都跟着变。
-
非static
- 非static修饰的属性是只能用“对象.”的方式访问;
- 非static修饰的属性是每个对象身上有一个该属性的变量,存放了该对象该属性的值。
从两者的第2点语法对比,我们领会Java语法设计者的设计意图。之所以让static修饰的属性能够用“类名.”的方式去访问,是为了体现这个属性是全类共享的。而非static修饰的属性用"对象."的方式去访问,是为了清楚的表达到底要操作是哪个对象的该属性。 在一个类当中,能够用static修饰的属性是不多见的,通常都需要在某个特定的问题域限制当中才找得到。static才是特例,通常都是非static的属性。
根据经验,只有常量,我们可以直接设计为static 原因:
-
常量属性的值是在类的定义中给定的,每个对象身上都是同样的值(常量也不能改);所以没有必要在每个对象身上存放一份,只需要全类共享一个就可以了。
-
常量属性的值在类定义中给定以后,外部也没有更改的权利,所以可以不用保密,直接public公布给外部看
-
通常常量属性的修饰符是:public static final的。
千万不要为了方便去把属性设计为static,一定要找到全类共享一个值的属性才做这种设计。
内存上的区别:
-
存放的位置不同 非static修饰的属性是在对象本身身上,也就是在堆当中; static修饰的属性没有存在对象身上,也就不是在堆当中;而是单独作为类的共享信息存放在一个叫做“静态区”的内存空间里;一个类的一个static属性只划分一个存放空间。
-
该属性在内存中产生的时机不同: 非static属性是在产生对象的时候,在内存中产生;static的属性是在 加载期 产生的于内存中。
两个凡是:
- 凡是用static修饰的内容,都是表示它与对象无关,只与类相关;
所以:用static修饰的属性,我们又叫做“类属性”/“静态属性”,没有static修饰的属性,我们一般叫做“成员属性”.
- 凡是用static修饰的内容,都会在加载期搞事情。
static 和 非static修饰的 方法
语法上的对比
-
有static的方法
-
在调用该方法的时候,可以通过“类名.”的方式来访问,也可以通过"对象."的方式来访问;只不过第2种方式不推荐,但不会报错;
-
在方法的内部实现中,静态方法只能调用本类的静态属性 或 静态方法;
之所以不能调用的原因: 原因1:静态方法是通过"类名."的方式调用,这个时候方法内部是没有当前对象这一目标,也就是说没有“this”,所以无法确认这里访问的非静态属性或方法到底是哪个对象的。 --- 这是站在面向对象的设计思想来表达的;
原因2:当我们在进行类的编译和加载的时候,在进行加载的顺序划分时,JVM都是首先加载静态的,然后再加载非静态的。如果在加载静态内容时,发现里面有非静态的调用,这个时候JVM是不认识这些非静态内容的,因此报错。反过来,在加载非静态的方法时,由于静态内容已经加载过了,所以JVM认识,因此通过。
-
-
非static的方法:
-
在调用该方法的时候,只能通过“对象.”的方式访问。
-
在方法的实现内部,即可以操作本类的静态属性和静态方法,也可以调用到本类的非静态属性和非静态方法;
这样的设计同样表明了用static修饰的方法是跟对象无关的;非static的方法是一定要确定到底是哪个对象去做这件事情。所以,用static修饰的方法又被称之为“类方法”或“静态方法”;非static修饰的方法被称为“成员方法”。
-
设计上的区别
我们要把工具类的工具方法,设计为static的;其他的都是非static的。 static 一定不能用来修饰 构造方法,因为 构造方法是用来创建对象的,而static天生就是用来表示与对象无关。
static 和 非static修饰 初始化块
对初始化块的简单认知
初始化块是我们可以在一个类里面定义的第4个内容。它的语法非常简单,就是直接在类里面,打上一对"{}
",然后在"{}
"里面书写代码。
它从外观形式上非常像一个没有方法声明,只有方法实现的特殊语句块。在它内部,我们可以书写任意指令语句,包括:输入输出、变量声明、运算表达、流程控制; 由于这个代码块是没有方法声明的,也不能接受参数和返回,所以很明显它是不能够在类的外部随意调用。 它的执行只能是在一个特定的时间点运行。即 产生对象的时候,所以它也被称之为“实例初始化块”。每产生一个对象,就会被执行一次。
实例初始化块到底在初始化一些什么呢?如果初始化对象的话,那么它的作用就和我们的构造方法重叠了。是的,确实是有这种情况,因此我们要探讨两个东西:
-
先后问题? 首先调用到构造方法,构造方法就会默认执行三个动作, 先在内存中划分空间(对象存放的空间); 然后在该空间划分属性; 接下来就会先执行我们在实例初始化块当中书写的代码; 再对属性进行初始化(这里的初始化是指有没有在声明属性的时候用代码赋初始值); 完了以后再执行我们在构造方法当中书写的代码。
-
哪些初始化动作放在初始化块,哪些动作是放在构造方法里面做? 构造方法里面的初始化,主要做的事通过外部调用者传入的参数,给当前对象的属性赋值; 如果某些属性不需要从外部接受参数赋初始值,那么我们更多的选用的是,在声明属性的时候直接使用“=”去赋值; 而实例初始化块更多的作用不是用来给对象中的属性赋初始值的,而是用来在一些特殊场景当中执行非赋值的初始代码,比如:开启资源,开启通讯管道,开启线程、开启文件等等。
初始化块也分static 和 非static的
前者也叫“静态初始化块”,后者叫做“实例初始化块”
-
"静态初始化块"是在加载类的时候被执行的,而一个类在JVM当中只需要加载一次,所以它是首先被执行,且以后无论产生多少对象,甚至不产生对象,也不再被执行。
-
“实例初始化块”是在产生对象的时候被执行,且产生多少个对象就会被执行多少次。
如果说“实例初始化块”的执行时机和次数与构造方法有一定的重叠,导致它的使用量很少。但是“静态初始化块”由于其独特的执行时机和执行次数,导致它是没有被替换的可能的。
很多资源的操作,包括开启呀,预加载呀,都是一次性完成的,没有必要每new一个对象就做一次,而且很多时候我们会把做这些动作放到程序的加载期而不是运行期,这是为了提升程序的运行速度。所以,在以后的使用当中,肯定会遇到书写“静态初始化块”的时机,但可能除了运气不好被面试官问以外,再也看不到操作不到“实例初始化块”。
静态初始化块也满足两个凡是:
-
与类有关; --- 类加载的时候被执行;
与对象无关;--- 不能在其内部访问对象的非静态内容,没有this当前对象,与产生对象与否无关。
-
加载期搞事情 --- 它搞的事情是在加载期被执行
内部类
故名思义 -- 就是在一个类A的内部定义另外一个类B,那么类A就是外部类,类B就是A的内部类。
首先需要明确:
-
内部类一定要定义在外部类的"{}"里面,而不是简单的写在一篇java文件当中;写在一篇java文件当中,它们两个是平行关系,不是嵌套关系。
-
内部类除了定义的位置有特殊性以外,它在本质上和普通的类没有区别; 包括:
- 仍然拥有属性、构造、行为、初始化块、甚至是它的内部类;
- 内部类虽然和外部类在同一篇java文件当中定义,但是编译以后它拥有自己独立的class文件。 对应的面试题是:是否一篇java文件编译后生成一篇class文件? 回答:这是错误的说法。应该是一个类一篇class文件。
- 内部类的类名,不是我们书写的简单名,而是和外部类的类名共同组成的。 对应的面试题是:class文件的名字是否与定义的类名保持一致? 回到:这是错误的说法。普通类是一致的,但是内部类不一致,内部类的class文件名要先添加上外部类的名字和“$”符号,后面再跟上内部类的名字
内部类的分类
首先根据内部类书写的位置,分为两大类:
-
成员内部类; 如果一个内部类是直接定义在外部类的“
{}
”当中,它的位置和外部类的属性、方法、构造,都处于平行位置,那么就叫做“成员内部类”。 当然作为外部类的成员,它是可以有访问修饰符的,用来控制这个内部类是否可以被外部使用。成员内部类编译后的class文件名:外部类名字$内部类的名字
-
局部内部类:
如果一个内部类是定义在外部类的某个方法当中,它的位置和这个方法的局部变量保持一致,那么就叫做“局部内部类”。 作为方法的内部定义,跟局部变量一样出了方法就认为消失不在了,所以它只能在这个方法内部使用,不能有访问修饰符。
局部内部类编译后的class文件名:外部类的名$序号内部类名字。这里序号从1开始,是同名局部内部类编译的顺序序号。
再根据语法特殊性,对“成员内部类”和“局部内部类”再分类:
- 成员内部类分为:普通成员内部类 和 静态内部类; 区别就在于 加不加 static 关键字;
- 局部内部类分为:普通的局部内部类 和 匿名内部类 所谓“匿名”就是没有给这个类起类名,匿名内部类是在定义的时候马上产生对象,只能使用一次。
四种内部类的使用语法
1、普通的成员内部类 要想用普通的成员内部类,首先要先产生外部类的对象,然后用"外部类对象.new" 的语法 去产生内部类对象。
OutClass1 out = new OutClass1();
OutClass1.InnerClass1 inner1 = out.new InnerClass1();
2、静态内部类 要用静态内部类,由于有static修饰符,所以这个静态内部类与它所属的外部类对象无关,因此可以不需要产生外部类对象,直接用外部类类名访问。
OutClass1.InnerClass2 inner2 = new OutClass1.InnerClass2();
3、局部内部类 局部内部类只能在定义它的方法之内使用。
InnerClass3 inner3 = new InnerClass3();
内部类当中书写this,代表的是当前的内部类对象,如果要表示它所关联的外部类对象,那么要写成“外部类类名.this”; 局部内部类的方法里面,能够操作到外部类的属性,但是不能操作到所属方法的局部变量(只能访问,不能改人家的值,当成常量用)。
4、匿名内部类 匿名内部类是在new对象的同时,去定义这个对象里面的属性和行为,然后由于没有给这个类型取名字,所以只能用这一次。
new Object(){
private int a;
public void test(){
}
}.test();
在new对象的同时,去定义这个对象里面有哪些属性和行为。由于这个对象有了新增的属性和行为,所以它不属于new关键字后面那个类型了,而是属于一种新的具有a属性和test方法的类型,但这种类型没有取名字。
继承 -- inherit
起源
在很多问题域当中,类与类之间除了has-a
`use-a这两种关系之外,还有一种“
is-a`”。
在我们举的场景中,普通员工、小时工、年薪员工 都是 员工。如果我们不考虑这层关系,那么设计的时候就会出现以下的问题:
-
三个员工类各自独立,表现不出它们具有is-a关系;
-
三个员工类当中的共有属性和行为,我们需要书写三次,在每个类当中都要写一次。
站在“分离与复用”的角度去看待,我们不希望三个员工类混在一起(希望各自分离);但同时共有的东西,包括属性和行为能够“复用”,至少我们的工作量。那么在这种情况下,我们提出“继承”的概念。
从“继承”在生活中的概念,我们能够轻易得到继承是发生在具有上下关系(或者叫父子关系)当中的,子不需要做任何努力直接就可以得到父所拥有的内容。 在面向对象编程当中,这个概念同样如此。我们可以把场景中的三种员工类进行再次分析,把它们共有的属性和行为提取出来,然后定义在一个叫做“员工”的类当中,这个类就是父类。
这样我们就可以得到一个基本的层次结构
第一层类 Employee 属性:
name
,num
,salary
行为:打卡第二层 小时工 属性:
hour
行为computePay
正式员工 属性socialSecurity
行为mailCheck
第三层(正式员工内) 普通员工 属性 行为
computePay
年薪员工 属性 行为computePay
千万不要因为两个类具有相同属性和行为就去做继承设计;而是要正确找到两个类具有is-a
关系,那么它们的相同属性和行为进行提取才是有意义的,才去设计继承!!
Java中继承的语法
Java里面继承的语法很简单,就是“extends”关键字
public class 子类 extends 父类 {
//子类的内容
}
属性在继承中的表现
结论:父类中的属性 在继承的情况,是肯定被放入到了子类对象当中。只是由于访问修饰符的限制,导致某些特殊访问修饰符的属性在子类中不能被直接访问。
继承在面向对象中体现的是“复用性”和“is-a”关系
- 访问修饰符
在Java当中一共提供了3个关键字,4种情况来表现访问修饰;
本类--MySelf | 同包非子类--MyWife | 同包子类--MySon | 非同包子类--MyDaughter | 非同包非子类--Stranger | |
---|---|---|---|---|---|
public--公共 | 能 | 能 | 能 | 能 | 能 |
protected--受保护 | 能 | 能 | 能 | 能 | |
不写(默认)--同包 | 能 | 能 | 能 | ||
private--私有 | 能 |
在这里看到,对于父类当中的私有属性,子类是看不见,但不代表它没有被继承。父类当中的同包属性,非同包子类是看不见,同样不代表它没有被继承。
-
父类 与 子类 当中定义了重名属性
-
这在语法上是可行,但是我们应该避免这种设计情况,因为它会给我们造成认知上的麻烦。正常的设计不应该出现这种情况。
-
这个时候在子类中由于拥有了两个重名属性,而不是用子类的属性 覆盖了 父类的同名属性。只是说,当我们直接用属性名在子类中操作的时候,它会根据就近原则默认为本类声明的属性;如果我们能加了this,那么在子类中写的this,仍然是子类的当前对象,所以仍然认为是子类定义的那个属性。
如果在这种情况下,我们一定要去访问父类中定义的同名属性,那么用把"this."修改为"super."。 这里我们引出了super这个关键字,如果说“this”代表的是当前对象,那么“super”代表的是当前对象的来自于父类的部分。
-
构造方法 在继承中的表现
结论:父类的构造方法是不会被子类继承的。 原因:
-
从语法层面上,构造方法的名字必须和类名保持一致。如果子类继承了父类的构造方法,那么相当于在子类里面有一个构造方法,但是该方法的名字是父类的名字。这样语法体系出现了冲突。
-
从场景来说,构造方法的作用是用来产生对象的。父类的构造方法是产生父类对象,子类构造方法是产生子类对象。如果子类继承了父类的构造方法,就相当于子类有方法用来产生父类对象了,这明显不合理。
虽然没有继承,但是父类的构造方法在继承的实现机制中起了不可替代的作用的
Java当中实现继承的本质 --- 内存叠加
当我们new一个子类对象的时候,它会首先调用父类的构造方法,产生父类对象部分,然后再调用子类的构造方法,在内存中父类对象的下面叠加上子类对象部分,从而形成一个完整的子类对象。
有可能的面试问题: 当我们new一个子类对象是不是产生了两个对象(一个父一个子)? 答案:不是。我们只产生了一个对象,就是子类对象。只是由于这个子类对象是由两部分组成的,一个是来自于父类的共有部分,然后叠加了来自子类的特有部分。
普通方法 在继承当中的表现
结论:父类当中的所有方法都会被子类继承,当然子类能不能调用到这个方法,仍然是受访问修饰符的影响。
方法的重写
如果子类中重新定义一个与父类一模一样的方法,书写上子类自己的实现,那么子类的方法就会覆盖父类的方法。 重写方法的规范要求:
-
方法名保持一致;
-
参数列表保持一致;
-
返回类型保持一致;
-
重写后的该方法访问修饰符不能比重写前小;
-
重写后不能比重写前抛出更多的异常,这里的更多指的是范围,不是个数。
方法重写很重要,是面向对象当中构建下一个特征”多态“的基本手段。
注意:static
修饰的方法只能被继承,但不能被重写。
重载与重写
- 相同的地方:
-
重载与重写都是指的方法,没有属性重载属性重写的说法;
-
他们体现的思想是一样,“相同的行为不同的实现”。
- 不同的地方:
-
方法重载是在一个类里面有多个重名方法,各有各的实现; 方法重写是体现在继承关系当中,父类有一个方法,子类继承后也有这个方法,但是子类的实现和父类不一样;
-
方法重载的语法要求: 方法名必须一样,参数列表必须不一样,与返回类型,访问修饰符无关;方法重写的要求见上面,比重载要高得多。
在场景设计当中去区分它们,比如: 1、程序员都有写代码的行为,Java程序员书写Java代码,前端程序员书写JS代码。 这是重写! 程序员、Java程序员、前端程序员,都有写代码的行为,但是不同的程序员子类各有各的实现
class 程序员{
public void codeing(){
}
}
class Java程序员 extends 程序员{
@Override
public void codeing(){
用Java代码实现
}
}
class 前端程序员 extends 程序员{
@Override
public void codeing(){
用JS代码实现
}
}
2、程序员都有书写代码的功能,给他笔记本就在笔记本上写,给他纸就在纸上写,什么都不给就在沙地上画 这是重载!只有一个类型程序员,行为都是写代码,根据外部传入的参数类型或个数的不同,各有各的实现
class 程序员{
public void codeing(){
沙地上画
}
public void codeing(纸 ){
纸上写
}
public void codeing(电脑){
电脑上写
}
}
3、人都有吃饭的行为,中国人用筷子,美国人用刀叉,印度人用手 ----重写 4、人都有吃饭的行为,给我筷子我夹着吃,给我勺子我挖着吃 ----重载
继承的层次结构
继承可以从上往下顺序继承,这个层次结构理论上可以有无限层。 直接继承关系,上层的叫父类,下层的叫子类; 非直接的继承关系,上层的叫“基类” 或 “超类”; 下层的叫“派生类”。 这是“纵向”的结构。
生活中,每个人除了有父,还有母,我们是从他们两个同时继承而来的。这种情况,我们把它叫做“多继承”。 但是,Java语言在设计的时候没有采纳“多继承”,而是使用的“单继承”,也就是说每个类能且只能有一个父类。不存在:
//报错
class 类 extends 父类, 母类{
}
在面向对象的设计思想当中,其实是没有限制只能做“单继承”的,有些编程语言(C++)选择了“多继承”,有些编程(Java)选择了“单继承”。因此,常常能看到面试官要求对“单继承”还是“多继承”做比较。
- 多继承
- 好处:具有更好的丰富度!
- 不好: 类结构会出现网状结构,增加了复杂度。
- 单继承
- 好处:类继承层次结构简单,都是树形结构
- 不好:灵活性不够,丰富度不够。
不同编程语言有不同的设计出发点,Java只不过是选择了“单继承”。然后,我们后面会学习Java里面设计的另外一个类型---接口,来弥补没有“多继承”缺失掉的丰富度。
实例分析
在一个游戏中,有盗贼,猎人,医生,农民,士兵几类角色, 这些角色都有名称、等级,体力,攻击力,防御力,金币, 还可以自己介绍自己,都可以攻击,防御; 其中: 属性计算方式: 体力 = 等级 * 角色加权(盗贼10,猎人12,医生8,农民8,士兵15); 攻击力 = 等级 * 角色加权(盗贼3,猎人2,医生1,农民1,士兵2); 防御力 = 等级 * 角色加权(盗贼1,猎人2,医生2,农民1,士兵3);
另外: 盗贼可以偷盗,一次偷盗可以得到12金,消耗体力9。 猎人可以打猎,一次打猎可以得到10金,消耗体力8。 农民可以采矿,一次采矿可以得到5金,消耗体力3。
医生可以治疗(可以治疗自己或士兵, 治疗后恢复的体力是医生等级的2倍)。 治疗自己不需要花金, 治疗对方要对方扣除恢复体力点数同样的金(如果没有不能治疗), 治疗后体力最多恢复到体力上限。
士兵可以狂暴(狂暴以后体力减少1半,攻击2,防御2)
设计这个系统。
拿到一个场景如何进行简单快速的OO分析: 1、找名词 游戏,盗贼,猎人,医生,农民,士兵 ,角色,名称、等级,体力,攻击力,防御力,金币,角色加权 2、在名词当中分析出哪些是我们要自定义的类型 游戏:在这个场景中,游戏不是一个具体的类型,而是整个场景的描述,我们可以看成是这个软件的整体名词。应该是整个项目叫做“游戏”,而不是这个项目中的某一个具体的类。 盗贼,猎人,医生,农民,士兵 :这些是在Java语言中没有的数据类型,需要我们自定义,包括它们有哪些属性,哪些行为。 角色:盗贼,猎人,医生,农民,士兵 都是角色。如果它们有共有属性和行为,我们是能够定义在角色身上的,所以角色如果要需要定义,那么它就是盗贼,猎人,医生,农民,士兵 的父类。 名称、等级,体力,攻击力,防御力,金币,角色加权:我们发现在Java语言中都有现成的数据类型表示,够用。 3、设计类与属性 角色:名称、等级,体力,攻击力,防御力,金币,三种角色加权 盗贼,猎人,医生,农民,士兵 : 没有特殊属性 4、找动词 自己介绍自己,攻击,防御,偷盗,打猎,采矿,治疗,狂暴 5、把行为关联到第三步设计的类当中去 角色:自己介绍自己,攻击,防御 盗贼:偷盗 猎人:打猎 农民:采矿 医生:治疗 士兵:狂暴 6、找类与类之间的关联关系 盗贼,猎人,医生,农民,士兵 is-a 角色 医生 在治疗行为中 use-a 士兵
继承中的类加载 -- 理论掌握
类加载动作是发生在我们程序代码执行之前,但是在代码中有一个东西是和类加载息息相关的 --- 类当中staic
修饰的内容。
其中特别是静态代码块,里面的代码是在加载期被执行的,所以我们需要知道静态代码块在继承关系下,所体现出来的特性。
需要掌握的结论:
-
当程序当中需要用到哪个类的时候,就会加载哪个类,也就会执行它的静态代码块; 用谁就加载谁,而不是写了谁就加载谁;
-
如果加载一个子类,那么会自动加载它的父类。因为子类代码中明确告知了有一部分共有属性和行为没有定义在子类本身身上,而是继承于父类。如果不加载父类,这部分共有的子类信息就会丢失掉。 加载的顺序:先父类,再子类。
-
一个类只加载一次! 如果父类已经被加载过了,那么加载子类的时候不会再去加载它。
-
现在的Java虚拟机在加载的时候采用的是JIT(just in time) --- 即时加载 其含义就是,它不是预先把要用到的类进行加载,全部完成以后才开始执行;而是边执行边加载,用到谁就加载谁。
this 和 super -- 开发中比较常用 面试也常出现
this关键字
this有两种用法: “this.
” 和 “this()
”
-
当作为
this.
的时候,this的含义指代的是当前对象,口语化就是指"我";我们用它主要是用来访问当前对象的属性和行为。这其中既包括的有定义在本类当中的属性和行为,也包括从父类继承的属性和行为。
用“
this.
”访问本类定义的内容,没有任何问题,都可以看;访问父类定义的内容,要受父类中该内容声明的访问修饰符的限制。 -
this()
的作用是在本类的一个构造方法当中,调用本类的另一个构造方法,达到构造方法当中代码 能够共享。 特点:this()
只能写在构造方法里面,且只能写在第一句;this()
是根据实参与形参匹配去寻找到底调用的是本类中的哪一个其他构造;- 不能够使用this()在构造方法当中形成“递归”
this()
只负责共享被调用构造方法里面的代码,而不会重新创建新对象。
super 关键字
super也有两种用法:"super.
" 和 “super()
”
-
当作为“
super.
”的时候,super的含义指代的是当前对象的父类对象部分,口语化指的是“我身上来自于父亲的那部分”用"
super.
"访问本类定义的内容,全部看不到;因为这些内容都是存在于子类特有部分的,而super看的从父类继承过来的那部分。访问父类定义的内容,能看到但同样要受父类中该内容声明的访问修饰符的限制。
总结:this.
能看到的,super.
不一定能看到;super.
能看到的,this.
一定能看到。因此,我们更多的时候使用的是this.
。
有一种情况,我们会使用"super." 如果子类重写了父类的某个方法,然后我们在子类中又想去调用这个方法在父类里面的实现,那么这个时候要用"super."。如果用"this.",你就在调用重写后的实现了。
super()
的作用是在子类的构造方法里面,指定调用父类的某一个构造方法。 特点:- 子类的构造方法里面就算一句代码都没有写,编译器也会自动加一个
super()
,用来默认调用父类的公共无参构造; super()
也是根据实参与形参的匹配去寻找到底调用的是父类的哪一个构造;super()
只能写在构造方法里面,也只能写在构造方法的第一句;这说明一个构造方法里面**this()
与super()
不能共存**,只能写一个。
- 子类的构造方法里面就算一句代码都没有写,编译器也会自动加一个
根类:Object -- 非常重要,这是唯一一个里面所有方法都要求Java程序员掌握的类
在Java当中,有一个非常特殊的类 -- Object,它的地位很特殊。它是所有类的根类(包括数组)。 任何一个类,如果我们没有主动写继承,那么就会让它默认继承Object这个类。作为一个祖宗类,根据类的继承层次结构,它里面定义的属性和方法,会被子子孙孙无条件的继承下去。
Object 既然是所有类的 根类,那么它的方法一定会被所有类拥有。在这个层面上讲,Object里面方法一定都是共用性最强的方法,是Java认为只要你是一个对象你就该有的方法。 这些方法有: equals --- 判断两个对象是否相等 toString --- 返回对象的字符串描述 hashcode --- 返回哈西值 getClass --- 获取类模版对象 clone --- 克隆 notify --- 唤醒线程 notifyAll --- 唤醒所有线程 wait --- 让线程等待 finalize --- 对象的销毁方法
equals 方法
当我们在比较两个对象是否相等的时候,我们一直以来都有一个运算符可以做这件事情。这个运算符就是所谓的"=="。 这个“==”,我们长期使用,基本数据类型的变量可以用来比较两个变量里面的值是否相等; 而引用数据类型的变量,用它比较的时候,是比较两个引用是否相等。什么是引用是否相等?就是这两个变量里面的引用有没有指向同一个对象,指向同一个得到true,没有指向同一个得到false。
但是还有一种更常见的情况,那就是我们要判断是两个对象里面的内容是否相等。这种情况下,"=="是无能为力的了。所以,这就是equals方法要做的事情:判断两个对象内容是否相等。
equals()
方法设计出来就是让程序员去**重写**的。因为,在当年写Object的时候,并不知道后续的程序员会书写什么样的子类(也就是说对子类的内容一无所知),所以他们只能规范:
- 所有的对象都有可能会有相互比较内容的情况发生,为了不让后续程序员各自发挥起各种各样的方法名,他们定义了一个专用的方法去规范;
- 由于不知道要具体后续程序员要如何比?这个比较规则是由具体业务确定的,所以他们暂时先写一个用"=="号比较的实现,以后交给子类的程序员去重写。
equals方法的使用:
-
什么时候用?如果我们需要判断本类的两个对象是否相等(内容相等或是业务相等),而不是指向同一个对象。那么我们不用自己去创建方法,而是重写来自于Object的equals方法。
-
怎么用?当然就是在这个类中重写这个方法,然后把需要实现的逻辑在它的方法体里面实现。
-
如果没有重写equals,那么Object当中的实现是"=="号。
toString 方法
先解除一个误会,很多初学者会误以为这个方法的作用是:把一个对象转换成字符串。其实不是,其作用是返回这个对象的字符串描述。
toString 方法的使用:
- 它是做什么的? 当我们对一个类的对象进行字符串操作的时候(直接打印一个对象,或直接拼接一个对象),那么JVM会自动调用该对象的
toString()
,把返回的字符串作为这个对象的描述信息进行使用。
2、咋用? 重写!把你需要看到的信息在该方法的实现内部进行拼接就可以了。
3、如果没有重写该方法,那么它的效果是: 类的限定名@对象的hash值
这里解释一下hash值:hash值是JVM根据hash算法,得到的每个对象在内存存放位置的唯一标识。 它不是内存地址,Java为了程序安全不会把内存地址公布给我们,你们可以想象成用了hash函数对这个值做了一次加密。这个hash值往往是用16进制的方式展示出来的。
finalize方法
垃圾回收机制的时机:GC通过发现一个对象没有引用指向了,那么它会根据自身算法选择时机完成销毁对象。
GC的出现其实是java专门的设计,作为它当初宣传的一个亮点。就是程序员不用关注销毁,只需要关注对象。而比如C++的编程语言就不是这样的。在C++的语法当中,每个类也有构造函数,同时还有一个特殊的函数--析构函数。这个函数与构造函数是一对,它的作用就是销毁C++中的对象的。而Java程序员在书写构造函数的时候只有构造函数,没有写过析构方法。没有写,不代表没有,其实是Java设计人员在根类Object当中把这个方法已经实现好了的,然后通过继承,所有的类都自动具备这个方法。这个方法就是finalize方法。
finalize方法是定义在Object当中,由GC在销毁对象的时候自动调用。这个方法一般我们不会去重写,也不会去调用,所以这个方法被设计为protected修饰符。它通常是出现在面试题目当中的。
一般问:
-
finalize方法是谁? ———Object的,通过继承所有类都有这个方法
-
finalize是谁调用的? ——— GC在销毁对象的时候调用
-
final、finally、finalize的比较
其中 final和finally是Java当中的两个关键字,finalize不是关键字,它是一个方法名
-
能不能重写finalize方法?———能,但一般不会重写这个方法。因为这个方法涉及到了比较多的底层操作,以及各种算法和异常处理,所以直接用是最好的。如果,要重写该方法,那么我们应该在重写代码中书写一句“
super.finalize()
”,让底层操作和算法等依然使用父类当中的实现,然后再添加上自己的实现。@Override public void finalize() throws Throwable{ super.finalize(); /* 后面跟上自己的实现 */ }
Object当中的finalize()从JDK9开始,正式被声明为“过时方法”
过时方法即这个方法现在已经不被使用了,有了新的替换。但是为了保证与之前的Java版本的过渡,就没有删除掉,但不建议使用。
如何让一个方法过时呢?只需要在这个方法的头上增加"@Deprecated"的修饰
@Deprecated public void test(){ }
在JDK9以后,Java是使用"
try-with-resouce
"和"Cleaner API
"来代替另外带"@"符号的标记为Java中的特殊数据类型——"注解"
final关键字 -- 知识点补全,final不仅仅是用来声明常量的
final关键字 —— final 的英文含义是“最终的”、“不变的”。所以用final修饰的内容都具有最终不变的特点。
-
final修饰的变量(包括属性、参数、) 就成为常量
int a = 10;//a是变量,值可以变 final int b = 100;//b是常量,值不可以变
-
final可以修饰方法,被其修饰的方法不能重写
记住:是不能重写,不是不能重载。重写才是对原方法进行实现部分的改变,重载是添加了一个(或多个)新的重名方法
用final修饰的方法,由于不能重写,所以我们叫它“最终方法”/“终态方法”
-
final可以修饰类,这种类不能被继承
我们可以把子类看成是对父类的改变,用了父类的属性和行为,也能把父类的行为进行重写改变。所以一个类被定义为final,那么它就不能够再生成新的子类,成为继承树上的某个分支的最终节点。
这样的类,称为“终态类”或“最终类”。
-
final不能修饰构造方法。因为构造方法本身就不能被继承,当然也就不能被重写,所以说也不存在被改变的情况
final 是一个修饰符,所以它应该写在修饰符的位置。
比如:
属性:修饰符 数据类型 变量名;
方法:修饰符 返回类型 方法名(参数列表)
如果同时有多个修饰符,修饰符与修饰符之间没有顺序可言
面向对象四大特征--多态、抽象
多态
多态的由来
多态在面向对象中的含义:相同的行为,不同的实现。
-
多态是描述行为的,也就是方法;
-
相同行为其实在本质上就是“同名”方法。
-
不同的实现:同名的方法,但是个各自有各自的方法实现体,也就是“
{}
”及其里面的代码不同。目前比较熟悉的两个与之有关的概念——“重载”和“重写”
多态在面向对象中体现的是“丰富度”。
重载与或单独使用重写都属于“静态多态”。而要做“动态多态”,需要两种技术的结合:重写 和 动态绑定
多态的分类
-
“ 静态多态”与“动态多态”
-
这里的“静态”和static关键字没有任何关系。
-
这里的“静”指的是在编译期就能够确定对象到底调用的是哪个方法;
-
“动”指的是程序必须在运行期才能够根据具体绑定的对象从而知道要调用的是哪个方法。
前面我们所学过的“重载”或者单独使用“重写”都属于“静态多态”。而要做“动态多态”,需要两种技术的结合:“重写”和“动态绑定”。
转型技术
所谓的“转型技术”就是类型转换。
基本数据类型转换
- 转换的依据
要求:小类型的可以直接转换为大类型的,大类型的需要强转为小类型的。这里的大小指的是该类型能够表示的数据范围大小
- 转换的规则
- 前提:boolean不参与数据类型转换
- 范围小的基本数据类型自动转换成范围大的基本数据类型
- 范围大的基本数据类型 强制 转换成 范围大的基本数据类型。语法:小类型变量 = (小类型)大类型的数据
如果不使用强转语法,编译不通过;如果使用强转语法,可能会用精度的丢失。
引用数据类型
- 转换的依据
要求:“小类型”的自动转换为“大类型”;“大类型”的要直接强制转换为“小类型”
这里的范围必须在“继承关系”下进行探讨,父类(超类)代表的范围比子类(派生类)要大。这种情况下的“大小”关系才是明确的、可以讨论的。
-
转换的规则 只有有继承关系的类才能够做转换,兄弟姐妹类都不允许。
-
在引用数据类型转换中,这种又被称为向上转型。因为它是沿着继承树往上走的。
Pet p = new Dog();
-
强制类型转换,又被称为向下转型。
Pet p = new Dog(); p = new Cat();
-
Cat c = (Cat)new Pet();//报错 ```
在引用数据类型中,强转仍然有风险,它的风险就不仅仅是精度丢失了,而是运行时报错"ClassCaseException"---类型转换异常
引用数据类型转换的本质
- 强转也好,自动类型转换也好,都不是改变对象本身,而只是换一个类型的引用变量去指向这个对象
- 编译或运行通不通过,依据是这个引用变量的类型和对象类型是否匹配。本质上只有两种情况是没有问题的:
- 本类引用指向本类对象
- 父类引用指向子类对象
从面向对象场景中解释:”一只宠物是一只狗对象“是完全正确的。从内存当中解释:每一个子类对象身上有一个完整的父类对象部分,所以用父类引用指过去是可以看到一个父类对象的完整内容,因此没有问题。
父类引用指向子类对象有何特点
- 父类引用指向子类对象后,只能看到来自于父类当中的属性或行为(当然要受到访问修饰符的影响)
- 如果这个行为被子类重写了,那么父类引用看到的这个行为,执行的效果是子类重写后的效果
父类引用指向子类对象后的弊端
由于变量的类型是父类类型,虽然指向了子类对象,但是在用变量做"."操作的时候,只能看到子类对象身上从父类继承而来的属性或方法,看不到子类特有的属性或方法。
解决方案: 第一步:通过强转,把父类的引用赋值给一个子类类型的变量。这样这两个引用都是指向的同一个对象,而父类类型变量能看到继承而来的属性和行为,子类类型的变量能看到所有的属性和行为。
第二步:强转是有风险的,而且引用类型的风险是运行时异常,它会导致程序停止运行直接报错。所以,在强壮前,我们必须通过判断,保证传进来的是一个可以被强转的类型。如何保证呢?
动态绑定机制
- 在面向对象程序开发中,我们将一个方法调用与该方法所在的类关联起来,称为“绑定”。
- 所谓动态绑定,是指在运行时根据具体对象的类型进行绑定。
- 当父类引用指向子类对象的时候,如果调用被子类重写的方法,那么表现的不是父类中的效果而是子类重写后的效果。
instanceof
instanceof
是一个关键字,同时它也是一个运算符!它是一个boolean
运算符!
书写格式: 对象 instanceof
类型
多态的应用
多态参数
当一个方法需要接收参数的时候,我们把形参的类型设计为父类类型,那么该父类下的所有子类对象都可以作为参数传递进来。
当然实际使用中,不一定是参数用父类类型的变量,属性也可以(has-a)
异构集合
已知数组有三大特点:
-
只能存同一类型的元素;
-
空间大小一旦申明不可变;
-
在连续内存空间中存放元素。
而我们又说了数组的三大特点也是三大缺点: 其中第2点,我们在前面讲封装的过程当中,通过自定义的“Array”类可以解决掉。
class Array{
private int[] array = new int[10];
public void add(int element){
自动往后面空白元素处放置;
如果空间不够,就先自动扩容
}
public void remove(int index){
自动删除该下标的元素,然后把该下标后面的元素,依次往前挪动。
如果空余空间太大,就自动缩容
}
//另外还提供了查询、修改、获取有效元素个数等三个方法。
}
学习多态之后,能够解决第一个问题。将数组元素的类型声明为父类类型,那么该数组就可以装所有的子类对象了。
父类[] 数组名 = new 父类[长度];
数组名[下标] = 子类对象;
当然更厉害的是Object[]
,那么由于Object是所有类类型和数组类型的根类,因此除了基本数据类型的元素,其他的类型的对象都可以放进去了。
Object[] objects = new Object[10];
objects[0] = "hello";
objects[1] = new Scanner(System.in);
objects[2] = new MachineGun();
objects[3] = new PureGirl("双儿");
objects[4] = new int[4];
objects[5] = new Object();
那如果我们把一和二的解决方法结合起来,就可以定义一个可变大可变小,也可以放置任意数据类型元素的集合了。 ---- 这就是所谓的“乾坤袋” 。
抽象
在面向对象当中,“抽象”到底属不属于OO的特征一直是一个被争论的问题。也就是说OO既有3大特征的说法,也有4大特征的说法。
其中长期以来面向对象专家(特别是国外),在他们的论文、书籍和阐述当中都只有“封装”、“继承”和“多态”三大特征。
四大特征说法其实是在2010年左右,在国内的软件公司面试题目当中出现的。他们增加了一个“抽象”。
在我的理解当中,面向对象中的“抽象”特征如果仅仅只是特指“abstract”,那么其范围和概念都过于小了一些,不足支撑它与“封装”、“继承”、“多态”一样称为编程思想的特性,而更多的是语言的特性。
而要想达到“编程思想”的特性,那么“抽象”的概念就不是OO所特有的了,包括面向过程也会有,它指的是把现实问题域当中的内容,通过人脑的抽取,进行数据和流程的建模,划分为计算机编程可以执行的方案。
抽象 -- abstract 关键字
abstract 关键字 是一个修饰符,意思就是“抽象”。
我们发现在很多场景当中,都会出现这么一种情况,所有子类都拥有某一个行为,但是各自的实现不一样。根据面向对象的分析,这种情况肯定要把这个共有行为设计到父类当中去。但尴尬的事情发生了,那就是父类虽然有这个行为,但是父类不知道该怎么实现。 那么在前面的操作过程中,我们采用了给这个方法打上一对"{}
",但是里面不写任何代码,让它做一个空白实现。
这里其实存在一个问题,一个方法是由方法的声明和方法的实现共同组成的。方法的实现就是那对“{}
”及其里面的内容。从编程语言的语法结构上来说,只要打上"{}
"就叫做这个方法已经实现了,只不过你的实现是什么指令都不发出。
而我们从“宠物”这例子当中,可以看到父类"Pet"只能确定它有这个“叫”的行为,不能确定这个行为的具体实现。因此在宠物这个类当中的叫的行为不应该实现,也就不应该打上这对“{}
”。
Java语言在设计的时候考虑到了这种情况,允许一个方法在声明的后面不接"{}
",而是用一个";
"直接告诉它,这个方法的书写到此为止。形如这样的方法,我们把它称之为“抽象方法”。而抽象方法必须用"abstract
"关键字修饰。
但“Pet”类还会再报错,通过查看报错信息,发现Java要求“有抽象方法的类必须是一个抽象类”。也就是说“Pet”类的声明处也要加上“abstract
”关键字。
什么又是抽象类呢?
抽象类指的的是一种特殊的类,最大的特点是不能产生对象,也就是不能 new。除此之外,抽象类在内容的定义上和普通类没有任何区别,能够定义:属性、构造、方法(抽象方法和非抽象方法)。
抽象类既然不能产生对象,为什么还需要构造方法呢? 我们目前定义类只有两种用法:1、产生对象;2、充当父类。 抽象类不能产生对象,那它就只能当父类了。也就是说抽象类的任务就是专门为子类规范有哪些共有属性和共有行为的。虽然它不能产生对象,但是它的子类是需要产生对象的,而new一个子类对象必然要求调用父类的构造方法,因为子类是不知道有哪些共有属性和行为的,这些代码都是写在父类身上的。所以,抽象类的构造方法不是自己用的,而是产生子类对象的时候去调用的。
抽象设计的好处是什么呢? 主要好处是把上层父类的设计意图强制的推广给下层类,要求下层子类必须重写哪些方法,省却程序员自己去记忆这个环节。 如果子类不重写父类的抽象方法,那么这个子类必须设计为抽象类,不然编译通不过。
总结一下: 语法上:
-
abstract关键字是用来修饰类 或 方法的;修饰类,这个类就叫“抽象类”;修饰方法,就叫“抽象方法”、
-
有抽象方法的类一定是抽象类; 抽象类在语法上可以没有抽象方法; --- 通常我们不会这么设计;
-
抽象类不能产生对象,只能充当父类; --- 这一点抽象类 与 最终类 刚好相反
-
子类必须重写抽象类当中的所有抽象方法,否则它也是抽象类;---- 不遵守就编译不过,这就是父类的设计意图强制子类去实现。
-
除了以上4点,抽象类在书写的时候与普通类没有不同:属性、构造、实现的方法、静态代码块都可以有。
场景当中:
- 抽象类是自然而然设计出来,就是当我们在提取共有行为到父类中的时候,发现这个行为在父类这个等级“只能确定有这个行为,不能确定这个行为的实现”,那么我们就把这个方法设计为”抽象方法“,这类设计为”抽象类“。
- 抽象类也是类,也可以声明变量,但是不能产生对象。那么它的赋值咋办呢?可以赋值为子类的对象 比如:Pet p = new Dog(); p = new Cat(); 这里体现出“多态”的特征。
抽象类
抽象类 在本质上就是一个加了“abstract
”修饰符的类。普通类能够有的内容,在它内部全部都有(属性、构造、方法、静态代码块、甚至内部类)。
如果要说它的特点的话就两个:
-
它可以拥有抽象方法;
-
它不能产生对象,只能充当父类。
它在实际的运用当中是充当设计人员构建继承关系的时候,首先设计的父类。但是,以后你们会发现,在实际开发当中,抽象类很少用。更多的用到的是下面的“接口”,甚至后面会发展成“面向接口”编程。
原因当然很多,但最重要的原因就是“抽象类也是一种类,那么在Java中它就只能做单继承”。
接口
关键字:“interface
”
接口是目前接触到的第三种引用数据类型,前两种是:类类型(class) 和 数组类型。
接口中的定义
接口的属性
接口的属性只能是公共静态的常量属性,也就是说只能是“public static final ”修饰的。由于修饰符是固定的,所以允许不写,默认就是公共静态常量。 这一特点说明,接口更多的是关注的“行为”,因为它的“属性”过于死板了,明显不常用。
接口的构造
接口当中不能定义构造方法。而没有构造方法,接口当中也是不能直接产生对象的。
接口的行为
接口里面的方法应该是公共的抽象方法。(后续JDK版本更新,也允许定义非抽象方法,但是这不是借口的主要用途或者设计的理由,而是后面的“锦上添花”)。就算我们不写“public abstract” 也会默认为公共抽象的。
以上三点说明:
-
接口不能产生对象;
-
接口只能充当在设计结构中的上层类型;
-
接口更多的关注的是行为,而不是属性。
接口与抽象类不同之处在于接口是可以做“多继承”或“多实现”的。
接口的使用
接口可以继承接口
使用关键字“extends
”可以让一个接口继承另一个接口。
效果:通过继承子接口自动拥有来自于父接口当中定义的属性和行为;同时还可以定义自己的属性和行为。
最特殊的是:接口不但可以继承接口,而且还是多继承。语法:
public interface 接口名 extends 父接口1,父接口2,.....{
}
如果两个父接口中都拥有一个一摸一样的方法,那么在子接口中不会冲突,因为子接口只需要拥有一个即可。 如果两个父接口中各自拥有一个同名的属性,那么子接口在使用的属性的时候会报错,因为分不清到底用的是哪个父接口定义的该属性。解决方案:在子接口中定义一个自己的同名属性。
类可以实现接口
类 实现 接口 使用关键字“implements”。这里的这个类被称为接口的“实现类”。
1、一个类实现接口以后,应该重写这个接口里面所有的抽象方法,否则这个类就是一个“抽象类”;
2、一个类可以同时实现多个接口,前提同样是必须实现这多个接口中的所有抽象方法,否则这个类仍然是一个“抽象类”;
3、一个类可以在实现接口的同时,继承父类。语法:
public class 类名 extends 父类 implements 父接口1, 父接口2,.....{
}
接口支持多态
“父类的引用可以指向子类的对象;接口的引用可以指向实现类的对象"。
接口同样可以在多态应用当中,体现出动态多态的效果。
同样还可以通过instanceof
关键字,来判断这个接口的引用是否指向某个类的对象
也可以通过强转的语法,把这接口引用强转成某个实现类的引用。同样,这里的强转也有风险,如果该引用不是指向接口的实现类对象,那么这个强转也会报错(ClassCastException).
结论:在没有继承关系的类当中,也能共享行为。这是接口的意义所在。
接口的应用场景
接口的场景
接口在使用上与抽象类有一定的相似度,但是又有很大的不同。抽象类与它的子类是“is-a”关系,但是接口与它的实现类不是。接口无论是它的定义的属性还是多实现的特点,它设计的目的主要是“让没有继承关系的类,也能共享行为”。接口,我们通常不把它叫做“is-a”关系,而是称为“like - a”关系,“像一个”。
标识接口
这种接口的设计目的已经不是让没有继承关系的共享行为了。通常,这种接口的"{}"当中没有任何内容,就是一个空白的实现。
这种接口是用来给类型打“标识”的,通过判断是否具有这个标识(用instanceof
判断),从而决定让某个类的对象可以从事某种操作,否则不允许。
为什么是标识接口,而不是标识类呢? 其核心原因就是因为在Java当中类是单继承的,接口是多实现的,且在实现接口的同时不影响继承父类。所以把标识设计为接口,不会对业务设计产生影响。
总结核心知识点
-
接口的地位 --- 独立的一种引用数据类型;
-
接口的语法
-
定义 关键字"interface" 属性只能是公共静态常量 没有构造方法 方法都是公共的抽象方法
-
使用 接口可以继承接口,多继承; 类可以实现接口,多实现。类应该把接口中的抽象方法全部重写,否则这个类又是抽象类。
-
跟封装的关系 让没有继承关系的类也能够共享行为 跟继承的关系 继承是 is - a, 与生俱来的行为写入继承 接口是 like-a 附属添加的行为写入接口 跟多态的关系 接口的引用可以指向实现类的对象!但只能看到该对象身上定义在接口里面的内容。可以强转,但是要先instanceof判断。
-
接口的使用场景
-
把附属添加的抽象行为定义在接口中,通过多实现让没有继承关系类具有共性行为;
-
标识接口。一种特殊的没有任何内容的接口,其目的是为了给某个类型打上标记,允许对它的对象执行某种操作。
-
-
常见面试题目:
-
抽象类和接口的区别;不要仅仅只谈论语法,还要谈论场景
-
接口隔离原则 也叫最小接口原则
-
在当前JDK的版本中,接口的新的语法
接口在JDK8版本当中,开始出现了一些新的语法设计。这些新语法的设计不见得是必须的,通常的目的是为了更简便应用,但是又引入了和接口设计初衷的一些违背。用不用?咋用?这个就见仁见智了。
接口当中允许定义非抽象方法
在接口中定义的非抽象方法,必须在方法声明处使用可选修饰符"default"来进行修饰。
效果:
接口中的default方法,不再强制要求实现类重写。 如果一个实现类实现了两个接口,而这两个接口当中都有一摸一样的default方法,那么这个default方法必须在实现类当中重写。
什么时候会用到接口中的default方法呢?
当没有继承关系的类共享某个行为,而该行为的实现也是共享的,这个时候就可以直接在接口里面把这个方法给实现了,不用让实现类再写一次。
接口当中可以定义static的方法
在接口当中可以定义static方法,而static方法不能是抽象的,所以也是要求要实现的方法。
接口中的static方法和default方法的区别?
static方法可以用"接口名."的方式访问,主要还是充当工具方法。 default方法必须用“实现类的对象.”的方式来访问,仍然跟对象有关。
**目前接口当中仍然不允许书写初始化块,无论是静态还是非静态的。 **
接口的提升内容 --- 匿名内部类
通过接口使用匿名内部类 是以后得最多的一种内部类。远远多于其他的内部类。
特点:
- 这个实现类没有取名字,所以只能new这一次
- 这个类的定义和new对象动作是同时编写的
- 整个定义作为一条new的语句,放在了外部类的某个方法里面,所以是一个内部类
Person p = new Person();
/*
传统写法,我们需要定义一个TestInterface的实现类
*/
p.function(new MyTest());
/*
如果这个方法的调用只执行一次,那么我们就没有必要去书写
一个单独的Java类TestInterface了。
在这种情况下,我们就可以使用匿名内部类的方式。
其语法特点是:在new对象的同时,用{}去定义这个类当中的属性和行为,包括重写。
这个对象是什么类型的呢?他是一个继承了Object,实现了TestInterface的类型,
但是我们没有给它取名字
*/
p.function( new TestInterface() {
@Override
public void test() {
System.out.println("匿名内部类中的test实现");
}
});
p.function(new TestInterface() {
@Override
public void test() {
}
});
设计中出现抽象方法
接口是书写抽象方法的类型,抽象类也是书写抽象方法的类型。经常有面试要求回答接口和抽象类的区别,网上大部分答案都只是整合了语法区别,而往往忘记了场景区别。
抽象类是类,所以它和子类是“一脉相承”的is-a关系;接口和它是实现类虽然也能共享行为,但不是is-a关系,更多的是“附属添加”的关系,用like-a。
对于我们来说设计在抽象类身上的抽象方法,秉承了类继承的“is-a”关系,所以应该是“与身俱来”的行为我们才定义在“抽象类”当中。---- 没有这个方法,就不是这个类型。
对于那些“附属添加”的行为就应该设计在接口当中,然后根据实际需要给某些类型进行添加,增加丰富度。 接口隔离原则 --- 在接口的设计上,不应该让上层接口污染下层接口或实现类。 当然它还有第二种说法: 最小接口原则 --- 尽量定义具有少量的方法的小接口,不要定义大而全的大接口。判断一个接口里面是否有多个方法的标准是这个方法应该同时出现或同时不出现。
转载自:https://juejin.cn/post/7088861053156589598