前言
由于近期需要用到基础以及底层的一些知识点,所以借此机会找来了一些学习的资料,用于对 JAVA
面向对象相关的知识点进行复盘、巩固,并且在此将一些重点的知识点做相关的记录,在后续的实际开发工作中遇到这些知识点相关的内容都会在本篇笔记中进行记录、迭代。
“ 温故而知新 ”
目录
本篇笔记的篇幅有点儿长,小伙伴们可以根据目录按需进行阅读。
学习主线
对于 “面向对象” 章节的学习,划分为以下三个阶段
- 1、
Java
类及类的成员:属性、方法、构造器:代码块、内部类 - 2、面向对象的三大特征:封装性、继承性、多态性
- 3、其他关键字的使用:
this
、super
、static
、final
、abstract
、interface
、package
、import
等
核心:掌握上述的概念、思想、关键字等如何在代码中进行具体实现与应用。
技巧:“大处着眼,小处着手”
具体涉及到的代码的需要特别细致的去写,否则一不小心就会出错,导致花费大量的时间去排错, 但也不能埋头去写,要关注前面以及后续的需要学习的一些知识点,区分哪些是重要的,哪些是不重要的,且如果遇到一些 "问题" 时,先花一些时间去考量这个问题的价值,如果需要花费大量的时间去完成,而到最后却不怎么用得上的话,那大可不必。
总结:抓重点去学习,将自己的时间和精力的价值最大化。
知识点复习
查阅资料的过程中发现的重点
阶段一
0x01:编程思想:面向过程&面向对象
面向过程(POP
)与面向对象的区别(OOP
)
两者都是一种编程思想,面向对象是由面向过程延伸而来的。
- 面向过程,强调的是功能的行为,以函数为最小的单位,重点考虑的是怎么做。
- 面向对象,将功能封装进对象,强调具备了功能的某个对象, 以类或者对象为最小的单位,考虑的是谁来做。
面向对象更加强调运用人类在日常生活中的思维逻辑所采用的思想方法以及原则,如抽象、分类、继承、聚合、多态等。
0x02:经典案例:把大象装进冰箱
面向过程:强调的是功能行为,以函数为最小单位,考虑怎么做。
- ① 把冰箱门打开
- ② 抬起大象,塞进冰箱
- ② 把冰箱门关闭
面向对象:强调具备了功能的对象,以类/对象为最小单位,考虑谁来做,以下面的伪代码来举例
人{
打开(冰箱){
冰箱.开开();
}
抬起(大象){
大象.进入(冰箱);
}
关闭(冰箱){
冰箱.闭合();
}
}
//实体对象
冰箱{
//对象拥有的功能
开开(){}
闭合(){}
}
大象{
进入(冰箱){}
}
0x03:理解 “万事万物皆对象”
在 Java
语言范畴中,我们都将功能、结构等封装到类中,通过类的实例化来使用类的具体功能。
- 例如
Scanner
、String
、File
等
涉及到 Java
语言与前端提交的数据或者后端的数据库交互时,在 Java
层面都体现为类、对象,例如在数据库中的每一个表都被看作为一个个的类,而每个表里面的字段都是这个类的属性。
0x04:内存解析的说明
引用类型的变量,只可能储存两类值:
null
- 内存地址值,包含变量(对象)的类型
例如,直接打印一个实例化后对象的值
Phone p = new Phone();
System.out.println(p)
得到的结果为
下图为对象数组在 JVM
的栈和堆中的内存的解析图
图片来源自宋红康老师课程的课件
0x05:匿名对象
创建两个匿名的对象,他们的内存空间是独立的,运行结束后地址会在内存中销毁(适合一次性使用的场景,无需在栈中储存该对象的内存地址)
new Phone().price = 1999;
new Phone().getPrice(); //0.0
匿名对象的使用
public class InstanceTest {
public static void main(String[] args) {
PhoneMall mall = new PhoneMall();
//匿名对象的使用
mall.show(new Phone());
}
}
class PhoneMall{
public void show(Phone phone){
//匿名对象通过形参的赋值,实现在方法中被多次调用
phone.sendEmail();
phone.playGame();
}
}
0x06:巧记 “方法的重载”
” 两同一不同 “
- 同一个类、相同的方法名
- 参数的列表不同:个数不同、类型不同
一个例子:
判断与 void show(int a,char b,double c){}
构成重载的有哪些?
void show(int x,char y,double z){} // no,参数的数量和类型相同
int show(int a,double c,char b){} // yes
void show(int a,double c,char b){} // yes
boolean show(int c,char b){} // yes
void show(double c){} // yes
double show(int x,char y,double z){} // 参数的数量和类型相同
void shows(){double c} // no ,方法名不同
总结
方法的重载需要重点关注相同方法名的参数的数量以及参数的类型
0x07:可变个数的形参
jdk 5.0 中新增的内容
使用过程中需要注意的:
- 可变形参的格式:数据类型 ... 变量名
- 当调用可变个数形参的方法时,传入的参数个数可以是:0个,1个, X个
- 可变形参的方法与本类中方法名相同时,形参不同的方法之间构成重载
- 可变形参的方法与本类中方法名相同时,形参类型也相同的数组之间不构成重载。换句话说,二者不能共存。
- 可变形参中必须声明在末尾
- 在方法的最多只能声明一个可变形参。
一个例子:
public class MethodArgsTest {
public static void main(String[] args) {
MethodArgsTest test = new MethodArgsTest();
test.show(new String[]{"AA","BB","CC"});
// 可变形参也可以接收一个数组
test.show(new String[]{"AA","BB","CC"});
}
public void show(String ... strs){
for(int i = 0;i < strs.length;i++){
System.out.println(strs[i]);
}
}
}
0x08:值传递机制
形参:方法定义时,声明在小括号内的参数
实参:方法调用时,实际传递给形参的数据
-
如果参数是基本的数据类型(例如
int
、float
、double
等)此时实参赋给形参的是真是储存的数据值。 -
当参数的类型为引用数据类型时,传递的是数据(对象)在栈指向堆中的内存地址,所以此时对该形参进行改动,实参也会发生改变
0x09:递归方法(recursion)
- 一个方法体内调用它自身
- 方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无需循环控制。
- 递归一定要向已知的方向递归,否则这种递归就变成了无穷递归,类似于死循环。
一些例子
例1:计算 1-100
之间所有自然数的和
public static void main(String[] args) {
RecursionTest test = new RecursionTest();
int sum = test.getSum(100);
System.out.println(sum);
}
public int getSum(int n) {// 3
if (n == 1) {
return 1;
} else {
return n + getSum(n - 1);
}
}
输出结果:5050
例2: 已知有一个数列:f(0) = 1,f(1) = 4, f(n+2) = 2 * f(n+1) + f(n)
,其中 n
是大于 0
的整数,求 f(10)
的值。
public static void main(String[] args) {
RecursionTest test = new RecursionTest();
int value = test.f(10);
System.out.println(value);
}
public int f(int n){
if(n == 0){
return 1;
}else if(n == 1){
return 4;
}else{
return 2*f(n - 1) + f(n - 2);
}
}
运行结果:10497
0x0A:封装与隐藏
为什么需要封装?
隐藏对象内部的复杂性,只对外公开简单的接口,便于外界的调用,从而提高系统的可扩展性、可维护性。
通俗的说,就是把该隐藏的都隐藏起来,该暴露的都暴露出来,这就是封装性的设计思想。
问题的引入
当我们创建一个类的对象以后,我们可以通过 "对象.属性
" 的方式对该对象的属性进行赋值。赋值操作要受到属性的数据类型和存储范围的制约。
但在实际问题中,我们给属性赋值时可能需要加入额外的限制条件,这个条件就不能在属性声明时体现,我们只能通过 方法 进行限制条件的添加,比如 setLegs()
同时,我们需要避免用户再使用 "对象.属性" 的方式对属性进行赋值。则需要将属性声明为私有的 (private
)
此时,针对于属性就体现了封装性。
封装性的体现
- 我们将类的属性
age
私有化 (private
),同时提供公共的 (public
) 方法来获取和设置此属性的值,例如:getAge()
和setAge()
- 不对外暴露的私有的方法
- 单例模式
需要权限修饰符来配合
Java
规定的 4
种权限(权重从小到大排列):
private
、缺省
、protected
、public
这4 种权限可以用来修饰类及类的内部结构有:
- 属性、方法、构造器、内部类
以下是这四种权限修饰符的权限范围
如果对类进行修饰,只能使用:缺省
、public
总结
Java
提供了 4
种权限修饰符来修饰类及类的内部结构,体现类及类的内部结构在被调用时的可见性的大小。
0x0B:构造器(构造方法)
构造器的作用:
- 创建对象
- 初始化对象的信息
一些特点
- 构造器的名称与类名相同
- 如果没有显式的定义类的构造器的话,则系统会默认提供一个空参的构造器
- 定义构造器的格式:
权限修饰符 类名(参数列表){}
- 一个类中定义的多个构造器,彼此构成重载
- 一旦我们显式的定义了类的构造器之后,系统就不再提供默认的空参构造器
- 一个类中,至少会有一个构造器
一个例子
public class Person {
private String name;
private int age;
private Date birthDate;
public Person(String n, int a, Date d) {
name = n;
age = a;
birthDate = d;
}
public Person(String n, int a) {
name = n;
age = a;
}
public Person(String n, Date d) {
name = n;
birthDate = d;
}
public Person(String n) {
name = n;
age = 30;
}
}
0x0C:this关键字
是什么?
this
可以修饰、调用类的属性、方法和构造器
- 在方法内部使用,即这个方法所属对象的引用
- 在构造器内部使用,表示该构造器正在初始化的对象
为什么?怎么用?
我们可以用 this
来区分属性和局部变量。
比如: this.name = name;
我们可以使用 "this.属性
" 或 "this.方法
" 的方式,调用当前对象属性或方法。但是,通常情况下,我们都选择省略 "this.
"。特殊情况下,如果方法的形参和类的属性同名时,我们必须显式的使用 "this.变量
" 的方式,表明此变量是属性,而非形参。
但是,通常情况下,我们都选择省略 "this.
",再特殊情况下,如果构造器的形参和类的属性同名时,我们必须显式的使用 "this.变量
" 的方式,表明此变量是属性,而非形参。
我们可以使用 this
调用构造器,案例如下
class Person{ // 定义Person类
private String name ;
private int age ;
public Person(){ // 无参构造器
System.out.println("新对象实例化") ;
}
public Person(String name){
this(); // 调用本类中的无参构造器
this.name = name ;
}
public Person(String name,int age){
this(name) ; // 调用有一个参数的构造器
this.age = age;
}
public String getInfo(){
return "姓名: " + name + ",年龄: " + age ;
}
}
需要注意以下几点
- 我们在类的构造器中,可以显式的使用"this(形参列表)"方式,调用本类中指定的其他构造器
- 构造器中不能通过 "
this(形参列表)
" 方式调用自己 - 如果一个类中有n个构造器,则最多有
n - 1
构造器中使用了 "this(形参列表
)" - 规定:使用this调用构造器时 "
this(形参列表)
" 必须声明在当前构造器的首行 - 构造器内部,最多只能声明一个 "
this(形参列表)
" 用来调用其他的构造器
0x0D:package关键字
是什么?
-
为了更好的实现项目中类的管理,在
java
中提供了包的概念 -
使用
package
声明类或者接口所属的包,需要声明在源文件的首行。 -
package
属于标识符,遵循标识符的命名规范,例如需要以小写进行命名,需要做到 "见名知意" 的效果,每 "." 一次,就代表一层文件目录 -
同一个包下,不能有同名的接口、类。
为什么,怎么用?
- 包帮助管理大型软件系统: 将功能相近的类划分到同一个包中。
- 包可以包含类和子包, 划分项目层次, 便于管理
- 解决类名名冲突的问题
- 控制访问权限
命名示例:package com.codeyee.javabase.object;
JDK中主要的包介绍
包名 | 描述 |
---|---|
java.lang | 包含一些Java语言的核心类, 如String、 Math、 Integer、 System和Thread, 提供常用功能 |
java.net | 包含执行与网络相关的操作的类和接口 |
java.io | 包含能提供多种输入/输出功能的类。 |
java.util | 包含一些实用工具类, 如定义系统特性、 接口的集合框架类、 使用与日期日历相关的函数。 |
java.text | 包含了一些java格式化相关的类 |
java.sql | 包含了java进行JDBC数据库编程的相关类/接口 |
阶段二
0x01:对继承性的理解
是什么?为什么?
继承性的好处
- 减少了代码的冗余,提高了代码的复用性
- 便于功能的扩展
- 为之后多态性的使用,提供了前提
- 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。
但是不要仅为了获取其他类中某个功能而去继承
怎么用?
继承性的使用格式
class A extends B{}
A: 子类、派生类、subclass
B: 父类、超类、基类、superclass
如何体现?
一旦子类A继承父类 B
以后,子类A中就获取了父类B中声明的所有的属性和方法。
我们可以通过下面的例子来理解
需要注意的是,父类中声明为 private
的属性或方法,子类继承父类以后,仍然获取了父类中私有的结构,只有因为封装性的影响,使得子类不能直接调用父类的结构而已。
子类继承父类以后,还可以声明自己特有的属性或方法,从而实现功能的拓展。
我们用下面的一个例子中体现上述的内容
public class Test{
public static void main(String[] args) {
Student stu = new Student();
stu.eat() //可以正常继承并且调用
stu.sleep() //由于该方法在父类中使用private进行修饰,无法调用
}
}
public class Person{
String name;
private int age;
private void sleep(){ //私有的
System.out.println("睡觉");
}
public void eat(){ //公共的
System.out.println("吃饭");
}
}
public class Student extends Person{
String major;
public void study(){
System.out.println("学习");
}
}
在Java中关于继承性的规定
- 一个类可以被多个子类继承。
Java
中类的单继承性:一个类只能有一个父类- 子父类是相对的概念。
- 子类直接继承的父类,称为:直接父类,而间接继承的父类称为:间接父类(所继承的父类又继承了其他类)
- 子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法
如果我们没有显式的声明一个类的父类的话,则此类继承于 java.lang.Object
类,所有的java类(除 java.lang.Object
类之外)都直接或间接的继承于java.lang.Object
类,意味着所有的 java
类具有 java.lang.Object
类声明的功能。
0x02:方法的重写(override/overwrite)
定义
在子类中可以根据需要对从父类中继承来的方法进行改造, 也称
为方法的重置、覆盖。在程序执行时,子类的方法将覆盖父类的方法。
要求
- 子类重写的方法必须和父类被重写的方法具有相同的
方法名称
、参数列表
- 子类重写的方法的返回值类型
不能大于
父类被重写的方法的返回值类型
- 子类重写的方法使用的访问权限
不能小于
父类被重写的方法的访问权限
- 子类方法抛出的异常不能大于父类被重写方法的异常
- 子类不能重写父类中声明为
private
权限的方法
需要注意
子类与父类中同名同参数的方法必须同时声明为非 static
的(即为重写),或者同时声明为
static
的(不是重写),因为 static
方法是属于类的,子类无法覆盖父类的方法。
下面我们来看一个例子
public class Person {
public String name;
public int age;
public String getInfo() {
return "Name: "+ name + "\n" +"age: "+ age;
}
}
public class Student extends Person {
public String school;
public String getInfo() { //重写方法
return "Name: "+ name + "\nage: "+ age
+ "\nschool: "+ school;
}
public static void main(String args[]){
Student s1=new Student();
s1.name="Bob";
s1.age=20;
s1.school="school2";
System.out.println(s1.getInfo());
}
}
输出结果:Name:Bob age:20 school:school2
从输出的结果我们可以看出 Student
类重写了 Person
类中的 getInfo()
方法
Person p1=new Person();
//调用Person类的getInfo()方法
p1.getInfo();
Student s1=new Student();
//调用Student类的getInfo()方法
s1.getInfo();
这是一种 “多态性”,同名的方法,用不同的对象来区分调用的是哪一个方法。
对于多态性的应用场景我们在后续的章节中详细的说道
0x03:四种访问权限修饰符
0x04:super关键字
在 Java
类中使用 super
来调用父类中的指定操作:
super
可用于访问父类中定义的属性super
可用于调用父类中定义的成员方法super
可用于在子类构造器中调用父类的构造器
注意
尤其当子父类出现同名成员时, 可以用 super
表明调用的是父类中的成员
super
的追溯不仅限于直接父类super
和this
的用法相像,this
代表本类对象的引用,super
代表父类的内存空间的标识
一些例子
1、关键字 super
举例:使用 super
调用父类的方法
class protected Person {
String name = "张三";
protected int age;
public String getInfo() {
return "Name: " + name + "\nage: " + age;
}
}
class Student extends Person {
protected String name = "李四";
private String school = "New Oriental";
public String getSchool() {
return school;
}
public String getInfo() {
//使用super调用父类的方法
return super.getInfo() + "\nschool: " + school;
}}
public class StudentTest {
public static void main(String[] args) {
Student st = new Student();
System.out.println(st.getInfo());
}
}
2、调用父类的构造器的例子
- 子类中所有的构造器默认都会访问父类中空参数的构造器
- 当父类中没有空参数的构造器时, 子类的构造器必须通过
this (参数列表)
或者super (参数列表)
语句指定调用本类或者父类中相应的构造器。 同时, 只能 ”二选一” 且必须放在构造器的首行 - 如果子类构造器中既未显式调用父类或本类的构造器, 且父类中又没有无参的构造器, 则编译出错
public class Person {
private String name;
private int age;
private Date birthDate;
public Person(String name, int age, Date d) {
this.name = name;
this.age = age;
this.birthDate = d;
}
public Person(String name, int age) {
this(name, age, null);
}
public Person(String name, Date d) {
this(name, 30, d);
}
public Person(String name) {
this(name, 30);
}
}
public class Student extends Person {
private String school;
public Student(String name, int age, String s{
super(name, age);
school = s;
}
public Student(String name, String s) {
super(name);
school = s;
}
// 编译出错: no super(),系统将调用父类无参数的构造器。
public Student(String s) {
school = s;
}
}
关键字 this
和 super
有哪些区别 ?
0x05:子类对象实例化的全过程
从结果上来看(继承性)
-
子类继承父类以后,就获取了父类中声明的属性或方法
-
创建子类的对象时在 堆空间 中会加载所有父类中声明的属性
从过程上来看
-
当我们通过子类的构造器创建子类时,我们一定会直接或者间接的调用其父类的构造器,进而调用父类的父类的构造器(所继承的父类也继承了其他类)直到调用了
java.lang.Object
类中的空参构造器为止 -
正因为加载过所有的父类结构,所以内存中会有父类的结构,这时子类对象才可以考虑进行调用,如下图所示
虽然创建子类对象是时调用了父类的构造器,但是自始至终只创建过一个对象,即为new的子类对象。
一个例子
class Creature {
public Creature() {
System.out.println("Creature无参数的构造器");
}
}
class Animal extends Creature {
public Animal(String name) {
System.out.println("Animal带一个参数的构造器,该动物的name为" + name);
}
public Animal(String name, int age) {
this(name);
System.out.println("Animal带两个参数的构造器,其age为" + age);
}
}
public class Wolf extends Animal {
public Wolf() {
super("灰太狼", 3);
System.out.println("Wolf无参数的构造器");
}
public static void main(String[] args) {
new Wolf();
}
}
输出结果如下
0x06:多态性
是什么?
何为多态性?在 java
中的多态性通常指的是对象的多态性,父类的引用指向子类的对象(或者子类的对象赋给父类的引用)也可以理解为一个事物的多种形态。
为什么?
提高了代码的通用性,常称作接口的重用
怎么用?
多态性的使用前提:
- 需要有类的继承关系
- 并且子类重写了父类的方法
有了对象的多态性以后,我们在编译期只能调用父类中声明的方法,但在运行期,我们实际执行的是子类重写父类的方法,如下代码
//对象的多态性:父类的引用指向子类的对象
Person p2 = new Man();
//多态的使用:当调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法(虚拟方法调用)
p2.eat();
p2.walk();
巧记多态性:编译时候看左边,运行时看右边
但是需要注意,虽然将子类 Man
的对象赋给了父类的对象实现多态,但是无法通过该对象调用子类特有的属性和方法(未经过重写的)
我们再来看一些多态性的例子,加深对多态性的理解
public class AnimalTest {
public static void main(String[] args) {
AnimalTest test = new AnimalTest();
test.func(new Dog());
}
public void func(Animal animal){
//Animal animal = new Dog();
animal.eat();
animal.shout();
}
}
class Animal{
public void eat(){
System.out.println("动物:进食");
}
public void shout(){
System.out.println("动物:叫");
}
}
class Dog extends Animal{
public void eat(){
System.out.println("狗吃骨头");
}
public void shout(){
System.out.println("汪!汪!汪!");
}
public void watchDoor(){
System.out.println("看门");
}
}
运行上面的代码
我们可以从上面的例子中看到,虽然 func
方法接收的参数是 Animal
类,但是由于 Dog
类继承了 Animail
类,并且重写父类的了 eat()
和 shuout()
方法,所以我们可以执行向 func
方法传入一个 Dog
类型的对象,而最终执行的也是 Dog
类重写后的方法。
这样做的好处是,可以根据我们传入不同的子类,来执行子类重写后的功能(方法)来实现多态,例如下面我们再构建一个 Cat
类,并且将 Cat 类的对象作为参数传入 func
方法
构建 Cat
类
class Cat extends Animal{
public void eat(){
System.out.println("猫吃鱼");
}
public void shout(){
System.out.println("喵!喵!喵!");
}
}
将 Cat
类型的对象作为参数传入 func
方法
public static void main(String[] args) {
AnimalTest test = new AnimalTest();
test.func(new Dog());
System.out.println("==================分割线====================");
test.func(new Cat());
}
执行结果如下
我们再来看一个更贴近我们日常应用的例子:数据库连接
class Driver{
public void doData(Connection conn){
conn.method1(); //数据库连接的一些相关操作的方法(假设)
conn.method2();
conn.method3();
}
}
我们使用的数据库可能是 Mysql
或者 Oracle
假设 Mysql
的连接类为 MySQlConnection
,Oracle
的连接类为 OracleConnection
,这两种数据库的连接类都继承于 Connection
,并且根据各自的特点重写了 method1
、method2
、method3
这三个方法(假设这三个方法是提供的是对数据库的连接操作)
如果我们现在需要连接到 Mysql
的数据库,只需要将 MySQlConnection
类对象作为 doData
的方法传入,就能实现对 Mysql
数据库连接相关的一些功能,例如
Driver driver = new Driver();
driver.doData(new MySQlConnection());
如果需求变更,需要我们完成对 Oracle
数据库的连接操作,这时我们只需传入一个 OracleConnection
类型的对象,就可以完成相关的操作,而无需再写额外的代码,例如
Driver driver = new Driver();
driver.doData(new OracleConnection());
虚拟方法调用(Virtual Method Invocation)
-
正常的方法调用
Person e = new Person(); e.getInfo(); Student e = new Student(); e.getInfo();
-
虚拟方法调用(多态情况下)
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的,例如
Person e = new Student(); e.getInfo(); //调用Student类的getInfo()方法
-
编译时类型和运行时类型
编译时
e
为Person
类型,而方法的调用是在运行时确定的,所以调用的是Student
类的getInfo()
方法,称为动态绑定。
如何理解动态绑定?
我们从编译和运行的角度看:
方法重载,是指允许存在多个同名方法,而这些方法的参数不同。 编译器根据方法不
同的参数列表
, 对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。 它们的调用地址在编译期就绑定了。 Java
的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。
所以: 对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,
这称为 “早绑定
” 或 “静态绑定
” ;
而对于多态,只有等到方法调用的那一刻, 解释运行器才会确定所要调用的具体
方法,这称为 “晚绑定
” 或 “动态绑定
” ,而 方法重写 就属于动态绑定的范畴。
引用一句 Bruce Eckel
的话:
“不要犯傻,如果它不是晚绑定, 它就不是多态。”
多态是编译时行为还是运行时行为?
我们先来看下面的例子
import java.util.Random;
class Animal {
protected void eat() {
System.out.println("animal eat food");
}
}
class Cat extends Animal {
protected void eat() {
System.out.println("cat eat fish");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("Dog eat bone");
}
}
class Sheep extends Animal {
public void eat() {
System.out.println("Sheep eat grass");
}
}
public class InterviewTest {
public static Animal getInstance(int key) {
switch (key) {
case 0:
return new Cat ();
case 1:
return new Dog ();
default:
return new Sheep ();
}
}
public static void main(String[] args) {shii
int key = new Random().nextInt(3);
System.out.println(key);
Animal animal = getInstance(key);
animal.eat();
}
}
我们从上述的代码例子中可以看到, 实例化哪个子类的对象是由变量 key
的值来决定的,而 key
的值由是在运行的时候才通过 Random
类来随机生成,所以由此可知,多态是 运行时
的行为。
多态性不适用于属性
对象的多态性,只适用于方法,而不适用于属性(编译和运行都看左边的类型)
例如我们在父类 Person
和 子类 Man
中都定义一个属性 id
,并且构建一个 Man
类型的对象赋值给 Person
类型的对象,再输出该对象的属性 id
的值,得到的为 Person
中所定义的属性值,如下代码
public class PersonTest {
public static void main(String[] args) {
Person p2 = new Man();
System.out.println(p2.id); //1001
}
}
public class Person {
int id = 1001;
}
public class Man {
int id = 1002;
}
关键字 instanceof
为什么?
有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法的,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法。子类特有的属性和方法不能调用。
那么 如何才能调用子类特有的属性和方法?
向下转型:使用强制类型转换符
我们来看一组代码
Person p2 = new Man();
Man m1 = (Man)p2;
m1.earnMoney();
m1.isSmoking = true;
如上代码所示,我们可通过使用 (Man)
来将 Person
对象强制转为 Man
类型,这是因为 Person
为 Man
的父类,并且在构建 Person
对象时实例化的是 Man
类,所以可以通过强制类型转换符进行 向下转型
我们再来看一个例子
Woman w1 = (Woman)p2; //编译通过
w1.goShopping(); //执行时出错,出现ClassCastException的异常。
但是在实际编码的过程中,使用强制转换可能会出现 ClassCastException
的异常,例如我们将对象 p2
强制转换为 Woman
类型,实际上是将包含 Man
类型的一些特有的方法和属性的 Person
对象强制转换为 Woman
类型,转换后得到了对象 w1
此时编译不会有任何错误,但是当通过对象 w1
调用 Woman
类的特有的方法 goShopping()
时,却抛出了异常,我们可以理解为两个各自有着自己不同特点的对象不能进行相互转换。此时我们的 instanceof
关键字就派上了用场。
是什么?怎么用?
instanceof 关键字的具体使用
使用格式:a instanceof A
: 判断对象 a
是否是类 A
的实例。如果是,返回 true
;如果不是,返回 false
使用情境:为了避免在向下转型时出现ClassCastException
的异常,我们在向下转型之前,先 进行 instanceof
的判断,一旦返回 true
,就进行向下转型。如果返回 false
,不进行向下转型。
对该关键字进行了解之后,我们将该关键字使用到上面的例子当中
if(p2 instanceof Woman){
Woman w1 = (Woman)p2;
w1.goShopping();
System.out.println("******Woman******");
}
if(p2 instanceof Man){
Man m2 = (Man)p2;
m2.earnMoney();
System.out.println("******Man******");
}
基本的数据类型转换与多态性下的对象转换的对比
0x07:浅谈Object类
是什么?
java.lang.Object
类Object
类是所有Java
类的根父类- 如果在类的声明中未使用
extends
关键字指明其父类,则默认父类为java.lang.Object
类 Object
类中的功能(属性、方法) 就具有通用性。- 没有属性
一些常见的方法
NO. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public Object() | 构造 | 构造器 |
2 | public boolean equals(Object obj) | 普通 | 对象比较 |
3 | public int hashCode() | 普通 | 取得Hash码 |
4 | public String toString() | 普通 | 对象打印时调用 |
“==” 与 equals() 的区别
1、回顾 ==
的使用:
==
:运算符
- 可以使用在基本数据类型变量和引用数据类型变量中
- 如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等。(不一定类型要相同)
如果比较的是引用数据类型变量,实际比较的是两个对象的内存地址值是否相同,即两个引用是否指向同一个对象实体。
补充:
==
符号使用时,必须保证符号左右两边的变量类型一致。
2、equals()
方法的使用:
-
是一个方法,而非运算符
-
只能适用于引用数据类型
-
Object
类中equals()
的定义:public boolean equals(Object obj) { return (this == obj); }
说明:
Object
类中定义的equals()
和==
的作用是相同的 -
像
String
、Date
、File
、包装类等都重写了Object
类中的equals()
方法。重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的 "值(实体内容)" 是否相同,如下代码//基本数据类型 int i = 10; int j = 10; double d = 10.0; System.out.println(i == j);//true System.out.println(i == d);//true,i的类型会自动提升为double //引用类型: Customer cust1 = new Customer("Tom",21); Customer cust2 = new Customer("Tom",21); System.out.println(cust1 == cust2); //false String str1 = new String("atguigu"); String str2 = new String("atguigu"); System.out.println(str1 == str2); //false System.out.println(cust1.equals(cust2)); //false System.out.println(str1.equals(str2)); //true Date date1 = new Date(32432525324L); Date date2 = new Date(32432525324L); System.out.println(date1.equals(date2)); //true
-
通常情况下,我们自定义的类如果使用
equals()
的话,也通常是比较两个对象的 "值(实体内容)" 是否相同。那么,我们就需要对Object
类中equals()
进行重写。重写的原则:比较两个对象的实体内容是否相同.
3、重写 equals()
通常情况下,我们自定义的类如果使用 equals()
的话也是想对比两个对象的值(实体内容)是否相同,所以我们需要对 Object
类中的 equals()
进行重写
public class Customer {
private String name;
private int age;
//...... 这里省略get set方法以及构造方法
//重写的原则:比较两个对象的实体内容(即:name和age)是否相同
//手动实现equals()的重写
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if(obj instanceof Customer){
Customer cust = (Customer) obj;
//对比每个属性的值
return this.age == cust.age && this.name.equals(cust.name);
}else{
return false;
}
}
在实际开发的过程中,我们会使用 eclipse
或者 idea
等开发工具去自动生成 equals()
的代码。
4、重写 equals()
方法的原则
- 对称性: 如果
x.equals(y)
返回是true
, 那么y.equals(x)也应该返回是true
。 - 自反性:
x.equals(x)
必须返回是true
。 - 传递性: 如果
x.equals(y)
返回是true
, 而且y.equals(z)
返回是true
,那么z.equals(x)
也应该返回是true
。 - 一致性: 如果
x.equals(y)
返回是true
, 只要x
和y
内容一直不变, 不管你重复x.equals(y)
多少次, 返回都是true
。 - 任何情况下,
x.equals(null)
, 永远返回是false
;
x.equals(和x不同类型的对象)
永远返回是false
。
为了防止出现空指针异常,在对象调用
equals()
方法之前,先判断该对象的值是否为null
0x08:浅谈 toString()
1、toString()
的基本使用
-
当我们输出一个对象的引用时,实际上就是调用当前对象的
toString()
-
Object
类中toString()
的定义:public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
-
像
String
、Date
、File
、包装类等都重写了Object
类中的toString()
方法。使得在调用对象的toString()
时,返回 "实体内容" 信息,如下代码所示public class ToStringTest { public static void main(String[] args) { Customer cust1 = new Customer("Tom",21); System.out.println(cust1.toString()); //输出结果:com.atguigu.java1.Customer@15db9742 System.out.println(cust1); //输出结果:com.atguigu.java1.Customer@15db9742 String str = new String("MM"); System.out.println(str); //输出结果:MM Date date = new Date(4534534534543L); System.out.println(date.toString()); //输出结果:Mon Sep 11 08:55:34 GMT+08:00 2113 } }
-
自定义类也可以重写
toString()
方法,当调用此方法时,返回对象的 "实体内容" ,如下代码,我们在自定义类中重写toString()
方法后再尝试输出对象的值public class Customer { // ... 省略属性等代码 @Override public String toString() { return "Customer [name=" + name + ", age=" + age + "]"; } }
再次输出该对象的值,观察输出的结果
System.out.println(cust1.toString()); //输出结果:Customer[name = Tom,age = 21] System.out.println(cust1); //输出结果:Customer[name = Tom,age = 21]
0x09:包装类(Wrapper)的使用
java
提供了 8
种基本数据类型对应的类,使得基本数据类型的变量具有类的特征,针对这八种基本数据类型定义相应的引用类型,这称作为包装类(封装类),有了类的特点,就可以调用类中的方法, 这样的 Java
才是真正的面向对象 。
本章节需要掌握的内容:基本数据类型、包装类、String
三者之间的相互转换
**装箱:基本数据类型包装成包装类的实例 **
-
通过包装类的构造器实现:
int i = 500; Integer t = new Integer(i);
-
还可以通过字符串参数构造包装类对象:
Float f = new Float(“4.56”); Long l = new Long(“asdf”); //NumberFormatException
拆箱:获得包装类对象中包装的基本类型变量
-
调用包装类的
.xxxValue()
方法boolean b = bObj.booleanValue();
JDK1.5之后,支持自动装箱,自动拆箱。但类型必须匹配。
字符串转换成基本数据类型
-
通过包装类的构造器实现:
int i = new Integer(“12”);
-
通过包装类的
parseXxx(String s)
静态方法:Float f = Float.parseFloat(“12.1”);
基本数据类型转换成字符串
-
调用字符串重载的
valueOf()
方法:String fstr = String.valueOf(2.34f);
-
更直接的方式:
String intStr = 5 + ""
一些练习
1、如下两份代码输出结果相同吗?各是什么?为什么?
代码一
Object o1 = true ? new Integer(1) : new Double(2.0);
System.out.println(o1);
代码二
Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
System.out.println(o2);
答案是不相同
代码一的执行结果为 1.0
,代码二的执行结果为 1
,题目考察的知识点为 类型提升 ,在代码1中使用了三元运算符,在 java
中涉及到预算的操作都需要保证左边与右边的类型存在继承关系或者相同,而 Integer 比 Double 类型小,所以被 向上转型
为 Double
类型,所以代码一的输出结果为 1.0
。
2、解释一下以下程序的输出结果
@Test
public void test3() {
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i == j);//false
Integer m = 1;
Integer n = 1;
System.out.println(m == n);//true
Integer x = 128; //相当于new了一个Integer对象
Integer y = 128; //相当于new了一个Integer对象
System.out.println(x == y);//false
}
解析:由于 Integer
内部定义了 IntegerCache
结构,IntegerCache
中定义了 Integer
类型的数组,并保存了从 -128~127
范围的整数。如果我们使用自动装箱的方式给 Integer
赋值的范围在 -128~127
范围内时,可以直接使用数组中的元素,不用再去 new
了 。
目的:提高效率
0x0A:阶段常见的一些问题
1、如何实现向下转型,需要注意什么问题?如何解决此问题
使用 (类型)
进行强制类型转换,可能会出现 ClassCastException
异常,使用 instanceof
在向下转型之前进行判断对象与类是否为同一类型。
2、== 和 equels() 有什么区别?
使用 ==
在值类型是对比两个变量的值,而在引用类型是对比两个变量的内存地址,使用 equels()
如果没有重写 Object
类的 equels()
的情况下,与 ==
的功能是相同的,String
和 Date
等类性默认重写了equels()
方法,其功能是对比两个实体的类型、属性是否相等,在自定义类需要重写 equels()
方法时也可以参考上述的逻辑,代码如下
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if(obj instanceof Customer){
Customer cust = (Customer) obj;
//对比每个属性的值
return this.age == cust.age && this.name.equals(cust.name);
}else{
return false;
}
}
阶段三
0x01:static关键字的使用
static
关键字可以用来修饰:属性、方法、代码块、内部类
使用 static 修饰属性:静态变量(或类变量)
属性,按是否使用 static
修饰,又分为:静态属性(变量) vs 非静态属性
-
实例变量:我们创建了类的多个对象,每个对象都独立的拥有一套类中的非静态属s性。当修改其中一个对象中的非静态属性时,不会导致其他对象中同样的属性值的修改。
-
静态变量:我们创建了类的多个对象,多个对象共享同一个静态变量。当通过某一个对象修改静态变量时,会导致其他对象调用此静态变量时,是修改过了的。
如下例子
public class StaticTest {
public static void main(String[] args) {
Chinese.nation = "中国";
Chinese c1 = new Chinese();
c1.name = "姚明";
c1.age = 40;
c1.nation = "CHN";
Chinese c2 = new Chinese();
c2.name = "马龙";
c2.age = 30;
c2.nation = "CHINA"; //使用c2调用该属性并修改值
System.out.println(c1.nation); //使用c1对象输出该属性的值,结果为CHINA
}
}
class Chinese{
String name;
int age;
static String nation;
}
static
修饰属性的其他说明:
-
静态变量随着类的加载而加载。可以通过 "
类.静态变量
" 的方式进行调用 -
静态变量的加载要早于对象的创建。
-
由于类只会加载一次,则静态变量在内存中也只会存在一份:存在方法区的静态域中。
调用类型 类变量 实例变量 类 yes no 对象 yes yes
类变量 vs 实例变量内存解析
使用static修饰方法:静态方法
随着类的加载而加载,可以通过"类.静态方法
"的方式进行调用
调用类型 | 静态方法 | 非静态方法 |
---|---|---|
类 | yes | no |
对象 | yes | yes |
-
静态方法中,只能调用静态的方法或属性
-
非静态方法中,既可以调用非静态的方法或属性,也可以调用静态的方法或属性
例如
public class StaticTest {
public static void main(String[] args) {
Chinese.show();
//Chinese.eat(); //编译不通过
//Chinese.info();
}
}
class Chinese{
String name;
int age;
static String nation;
public void eat(){
System.out.println("中国人吃中餐");
//调用非静态结构
this.info();
System.out.println("name :" +name);
//调用静态结构
walk();
System.out.println("nation : " + nation);
}
public static void show(){
System.out.println("我是一个中国人!");
//不能调用非静态的结构
// eat();
// name = "Tom";
//可以调用静态的结构
System.out.println(Chinese.nation);
walk();
}
}
还需要注意
- 在静态方法内,不能使用
this
关键字、super
关键字 - 关于静态属性和静态方法的使用,大家都从生命周期的角度去理解。
开发中,如何确定一个属性是否要声明为static的?
-
属性是可以被多个对象所共享的,不会随着对象的不同而不同的。
-
类中的常量也常常声明为
static
开发中,如何确定一个方法是否要声明为static的?
-
操作静态属性的方法,通常设置为
static
的 -
工具类中的方法,习惯上声明为
static
的。 比如:Math
、Arrays
、Collections
0x02:单例设计模式
什么是设计模式?
设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、
以及解决问题的思考方式。 设计模免去我们自己再思考和摸索。就像是经典
的棋谱,不同的棋局,我们用不同的棋谱。 ”套路”
那么什么是单例设计模式?
- 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
- 如果我们要让类在 一个虚拟机中只能产生一个对象 ,我们首先必须将类的构造器的访问权限设置为
private
,这样,就不能用new
操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。 - 因为在类的外部开始还无法得到类的对象,只能调用该类的 **某个静态方法 **以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类/内部产生的 该类对象的变量也必须定义成静态的。
如何实现?
饿汉式单例模式的实现
public class SingletonTest1 {
public static void main(String[] args) {
Bank bank1 = Bank.getInstance();
Bank bank2 = Bank.getInstance();
System.out.println(bank1 == bank2); //同一个对象,所以结果为true
}
}
//饿汉式
class Bank{
//1.私有化类的构造器
private Bank(){
}
//2.内部创建类的对象,要求此对象也必须声明为静态的
private static Bank instance = new Bank();
//3.提供公共的静态的方法,返回类的对象
public static Bank getInstance(){
return instance;
}
}
1、声明私有化的类的构造器
2、内部创建类的对象,要求此对象也必须声明为静态的
3、声明 public
、static
的方法,返回当前类的对象
“懒汉式” 单例模式的实现
public class SingletonTest2 {
public static void main(String[] args) {
Order order1 = Order.getInstance();
Order order2 = Order.getInstance();
System.out.println(order1 == order2);
}
}
class Order{
//1.私有化类的构造器
private Order(){
}
//2.声明当前类对象,没有初始化,此对象也必须声明为static的
private static Order instance = null;
//3.声明public、static的返回当前类对象的方法
public static Order getInstance(){
if(instance == null){
instance = new Order();
}
return instance;
}
}
1、私有化类的构造器
2、声明当前类对象,没有初始化,此对象也必须声明为 static
的
3、声明 public
、static
的返回当前类对象的方法
区分饿汉式 和 懒汉式
饿汉式:
-
坏处:对象加载时间过长(相对于懒汉式 )。
-
好处:饿汉式是线程安全的
懒汉式:
-
好处:延迟对象的创建。
-
目前的写法坏处:线程不安全。---> 到多线程内容时,再做修改
0x03:代码块结构
代码块的作用:初始化类、对象
如果要对代码块有修饰的话,自能使用 static
代码块分为两种:静态代码块、非静态代码块
-
静态代码块
-
① 内部可以有输出语句
-
② 随着类的加载而执行,而且只执行一次
-
③ 作用:初始化类的信息
-
④ 如果一个类中当以了多个静态代码块,则按照声明的顺序来执行
-
⑤ 静态代码块的执行要优先于非静态的代码块
-
⑥ 静态代码块的内部只能调用静态的属性、静态的方法,不能调用非静态的结构
-
-
非静态的代码块
- ① 内部可以有输出语句
- ② 随着对象的创建而执行
- ③ 每创建一个对象,就执行一次非静态的代码块
- ④ 如果一个类中定义了多个非静态的代码块,则按照声明的顺序先后执行
- ⑤ 非静态的代码块可以同时调用静态的和非静态的属性和方法
我们来开一组代码,结合输出的内容加深对上述知识点的了解
public class BlockTest {
public static void main(String[] args) {
String desc = Person.desc;
System.out.println(desc);
Person p1 = new Person();
Person p2 = new Person();
System.out.println(p1.age);
Person.info();
}
}
class Person{
//属性
String name;
int age;
static String desc = "我是一个人";
//构造器
public Person(){
}
public Person(String name,int age){
this.name = name;
this.age = age;
}
//非static的代码块
{
System.out.println("hello, block - 2");
}
{
System.out.println("hello, block - 1");
//调用非静态结构
age = 1;
eat();
//调用静态结构
desc = "我是一个爱学习的人1";
info();
}
//static的代码块
static{
System.out.println("hello,static block-2");
}
static{
System.out.println("hello,static block-1");
//调用静态结构
desc = "我是一个爱学习的人";
info();
//不可以调用非静态结构
// eat();
// name = "Tom";
}
//方法
public void eat(){
System.out.println("吃饭");
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
public static void info(){
System.out.println("我是一个快乐的人!");
}
}
输出结果:
hello,static block-2
hello,static block-1
我是一个快乐的人!
我是一个爱学习的人
hello, block - 2
hello, block - 1
吃饭
我是一个快乐的人!
hello, block - 2
hello, block - 1
吃饭
我是一个快乐的人!
1
我是一个快乐的人!
代码块的使用场景举例
我们通过一个获取数据库连接的案例来理解一下代码块的应用
public class JDBCUtils {
private static DataSource dataSource = null;
static{
InputStream is = null;
try {
is = DBCPTest.class.getClassLoader().getResourceAsStream("dbcp.properties");
Properties pros = new Properties();
pros.load(is);
//调用BasicDataSourceFactory的静态方法,获取数据源。
dataSource = BasicDataSourceFactory.createDataSource(pros);
} catch (Exception e) {
e.printStackTrace();
}finally{
if(is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//使用DBCP数据库连接池实现数据库的连接
public static Connection getConnectionAccess() throws SQLException{
Connection conn = dataSource.getConnection();
System.out.println(conn);
return conn;
}
}
我们在获取数据库的连接对象之前,需要构建一个数据库连接池,但是为了节省系统的开销,我们一个类里面只构建一个数据库,这就可以使用惊天代码块的特性:只在类加载时创建一次,在这之后我们每调用一次getConnectionAccess()
即可从连接池中获取一个连接对象,然后对数据库进行相关的操作
对于属性可以赋值的位置
- ① 默认初始化
- ② 显示初始化
- ③ 构造器中初始化
- ④ 有了对象以后,通过 “对象.属性(方法)” 的方式,进行赋值
- ⑤ 在代码块中赋值(代码块其中的一个使用场景)
执行的先后顺序:① --> ②或⑤ --> ③ --> ④
0x04:final关键字
final:最终的
final
关键字可以用于修饰的结构有:类、方法、变量
- 此类不能被其他类所继承
- 比如:
String
类、System
类、StringBuffer
类 - 寓意:不用(不能)在本类的基础上再去扩充其他的功能
使用 final
关键字来修饰方法:
- 表明此方法不可以被重写,例如下图所示
-
例如:
Object
类的getClass();
我们发现
getClass()
方法中还用到一个关键字native
,使用该关键字表示该方法的具体使用调用的是调用了c/c++
的实现,而非java
实现。
使用 final
关键字来修饰变量(属性)
-
表示该变量为一个 “常量” ,该变量的值不可被修改,如下图
-
被
final
修饰的属性可以被初始化的位置- 在定义属性时显示初始化
- 在代码块中初始化
- 在构造器中初始化
例如
public class FinalTest { final int WIDTH = 0; final int LEFT; final int RIGHT; { LEFT = 1; } public FinalTest(){ RIGHT = 2; } public FinalTest(int n){ RIGHT = n; } }
如果类中存在多个构造器,则每个构造器中都需要对被
final
所修饰的未初始化的属性进行赋值。 -
用
final
修饰方法的形参表示此形参是一个常量,当我们调用此方法时,给常量赋一个实参,一旦赋值后,就只能在方法体内使用此形参,但不能进行重写赋值。
使用 static final
用来修饰:全局常量
- 在接口中比较常见
- 用于修饰方法:可以直接类来进行调用,并且该方法不能重写
常见的一些排错情况:
-
场景1
上述的代码
++x
是一个错误的举例,无法通过编译,原因是对常量x
进行了自增操作,但x+1
只是将x
+1
后的值进行返回,而没有对x
本身进行赋值操作 -
场景2
在上述的代码当中,对象
o
只是常量,但是该对象的属性i
是一个普通的属性,可以被修饰,但是如果执行o = new Other()
则是对该对象进行了修改,编译则不通过。
0x04:abstract 关键字的使用
abstract: 抽象的
随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做 抽象类。
abstract
关键字可以用来修饰的结构有:类、方法
-
使用
abstract
关键字修饰类:抽象类 -
抽象类类不能被实例化
-
抽象类中一定有构造器,便于子类实例化时调用(涉及到的知识点为:子类对象实例化的全过程)
-
在实际开发中,都会提供抽象类的子类,使用子类对象进行实例化,完成相关的操作
-
使用
abstract
修饰方法:抽象方法- 抽象方法只有方法的声明,没有具体的实现(方法体)
- 包含抽象方法的类,必须是一个抽象类。反之,抽象类中可以没有抽象方法
- 若子类重写了父类的所有抽象方法,此类才可以被实例化,否则该子类也是一个抽象类,需要使用
abstract
关键字修饰
-
abstract
使用时需要注意的几个点abstract
不能用来修饰:属性、构造器等结构abstract
不能用来修饰:私有方法、静态方法、final
的方法、final
的类
如下例子
abstract class Creature{
public abstract void breath();
}
abstract class Person extends Creature{
String name;
int age;
public Person(){
}
public Person(String name,int age){
this.name = name;
this.age = age;
}
//不是抽象方法:
// public void eat(){
//
// }
//抽象方法
public abstract void eat();
public void walk(){
System.out.println("人走路");
}
}
class Student extends Person{
public Student(String name,int age){
super(name,age);
}
public Student(){
}
public void eat(){
System.out.println("学生多吃有营养的食物");
}
@Override
public void breath() {
System.out.println("学生应该呼吸新鲜的没有雾霾的空气");
}
}
抽象类的应用
抽象类是用来模型化那些父类无法确定全部实现,而是由其子类提供具体实现的对象的类。
-
在航运公司系统中,
Vehicle
(车辆)类需要定义两个方法分别计算运输工具的燃料效率和行驶距离。如下图卡车(
Truck
)和驳船 (RiverBarge
) 的燃料效率和行驶距离的计算方法完全不
同。Vehicle
类不能提供计算方法,但子类可以,但又需要子类又必须拥有这些计算方法,所以需要用到abstract
关键字。 -
解决方案
Java
允许类设计者指定:超类声明一个方法但不提供实现,该方法的实现由子类提供。这样的方法称为抽象方法。有一个或更多抽象方法的类称为抽类。 -
在代码中实现
Vehicle是一个抽象类,有两个抽象方法。
public abstract class Vehicle{ //计算燃料效率的抽象方法 public abstract double calcFuelEfficiency(); //计算行驶距离的抽象方法 public abstract double calcTripDistance(); } public class Truck extends Vehicle{ public double calcFuelEfficiency( ) { //写出计算卡车的燃料效率的具体方法 } public double calcTripDistance( ) { //写出计算卡车行驶距离的具体方法 } } public class RiverBarge extends Vehicle{ public double calcFuelEfficiency( ) { //写出计算驳船行驶距离的具体方法 } public double calcTripDistance( ) { //写出计算驳船行驶距离的具体方法 } }
抽象类的匿名子类
作用:无需再造一个具体的子类去实现抽象类的方法
语法格式
new 抽象类名(){
@Overrride
public void 方法1(){
}
@Overrride
public void 方法2(){
}
}
具体例子
/*
* 抽象类的匿名子类
*
*/
public class PersonTest {
public static void main(String[] args) {
method(new Student());//匿名对象
Worker worker = new Worker();
method1(worker);//非匿名的类非匿名的对象
method1(new Worker());//非匿名的类匿名的对象
System.out.println("********************");
//创建了一匿名子类的对象:p
Person p = new Person(){
@Override
public void eat() {
System.out.println("吃东西");
}
@Override
public void breath() {
System.out.println("好好呼吸");
}
};
method1(p);
System.out.println("********************");
//创建匿名子类的匿名对象
method1(new Person(){
@Override
public void eat() {
System.out.println("吃好吃东西");
}
@Override
public void breath() {
System.out.println("好好呼吸新鲜空气");
}
});
}
public static void method1(Person p){
p.eat();
p.breath();
}
public static void method(Student s){
}
}
class Worker extends Person{
@Override
public void eat() {}
@Override
public void breath() {}
}
思考一些问题
1、为什么抽象类不可以使用 final
关键字声明?
抽象类必须要求用子类去继承和实例化,使用 final
则无法被继承
2、一个抽象类中可以定义构造器吗?
可以,子类在实例化的时候还是会调用父类的构造器
0x05:模板方法设计模式(TemplateMethod)
抽象类的体现就是一种 模板模式的设计模式 ,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。
解决的问题:
- 当功能内部的一部分实现是已经确定的,一部分是不确定的,这时可以把不确定的部分暴露出去,让子类去实现。
- 换句话说,在软件开发中实现一个算法时,整体的步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分是易变的,易变的部分可以抽象出来,提供给不同的子类实现。这就是一种模板模式
我们来看一下具体的例子,来体会以上上述的内容
//抽象类的应用:模板方法的设计模式
public class TemplateMethodTest {
public static void main(String[] args) {
BankTemplateMethod btm = new DrawMoney();
btm.process();
BankTemplateMethod btm2 = new ManageMoney();
btm2.process();
}
}
abstract class BankTemplateMethod {
// 具体方法
public void takeNumber() {
System.out.println("取号排队");
}
public abstract void transact(); // 办理具体的业务 //钩子方法
public void evaluate() {
System.out.println("反馈评分");
}
// 模板方法,把基本操作组合到一起,子类一般不能重写
public final void process() {
this.takeNumber();
this.transact();// 像个钩子,具体执行时,挂哪个子类,就执行哪个子类的实现代码
this.evaluate();
}
}
//业务子类,实现各自业务的办理流程
class DrawMoney extends BankTemplateMethod {
public void transact() {
System.out.println("我要取款!!!");
}
}
class ManageMoney extends BankTemplateMethod {
public void transact() {
System.out.println("我要理财!我这里有2000万美元!!");
}
}
从上述的代码中,我们把银行的业务抽象成了一个 BankTemplateMethod
类,由于每种业务的办理流程都不同,所以定义了一个抽象的方法 transact
用于给不同的 “业务子类” 去实现具体的业务办理流程,在父类中再定义一个 process
方法去规范化银行业务办理的流程,并且该方法使用 final
去修饰,使子类无法重写该方法。
0x06:接口(interface)
一个类可以实现多个接口,在一定程度上解决了我们类的单继承性的局限性
接口的基本使用
- 一方面, 有时必须从几个类中派生出一个子类, 继承它们所有的属性和方
法。 但是,Java
不支持多重继承。 有了接口, 就可以得到多重继承的效果。 - 另一方面, 有时必须从几个类中抽取出一些共同的行为特征,而它们之间又
没有is-a
(继承)的关系,仅仅是具有相同的行为特征而已。例如:鼠标、键盘、打印机、扫描仪、摄像头、充电器、 MP3机、手机、数码相机、移动硬盘等都支持USB
连接。
接口 (interface) 是抽象方法和常量值定义的集合。
接口的一些特点:
- 用
interface
来定义,与class
同级 。 - 接口中的所有成员变量都默认是由
public static final
修饰的。 - 接口中的所有抽象方法都默认是由
public abstract
修饰的。 - 接口中没有构造器。
- 接口采用多继承 机制。
接口定义的举例如下:
public interface Runner {
int ID = 1;
void start();
public void run();
void stop();
}
在上述的定义当中,系统默认会将接口中定义的属性修饰为常量,而方法会修饰为抽象的方法,我们在手动修饰时也必须遵守这一规则。
public interface Runner {
public static final int ID = 1;
public abstract void start();
public abstract void run();
public abstract void stop();
}
通过下面的几个图,我们来进一步了解接口的作用
从上图总我们可以看出,运动员和学生的子类都共同拥有学习的技能,但是由于学生和运动员不存在继承的关系,所以我们就可以使用接口来实现。
飞机、子弹、风筝、热气球都共同拥有 “飞” 的技能,但 "子弹" 在拥有 "飞" 的技能的同时还拥有了 "攻击性" 这个技能。
-
接口就是规范,定义的是一组规则,体现了现实世界中 “如果你是/要...则必须能... ” 的思想。 继承是一个 "是不是" 的关系,而接口实现则是 "能不能" 的关系。
-
接口的本质是契约,标准,规范,就像我们的法律一样。制定好后大家都要遵守。
代码示例如下
public class InterfaceTest {
public static void main(String[] args) {
System.out.println(Flyable.MAX_SPEED);
System.out.println(Flyable.MIN_SPEED);
// Flyable.MIN_SPEED = 2;
Plane plane = new Plane();
plane.fly();
}
}
interface Flyable{
//全局常量
public static final int MAX_SPEED = 7900;//第一宇宙速度
int MIN_SPEED = 1;//省略了public static final
//抽象方法
public abstract void fly();
//省略了public abstract
void stop();
}
interface Attackable{
void attack();
}
class Plane implements Flyable{
@Override
public void fly() {
System.out.println("通过引擎起飞");
}
@Override
public void stop() {
System.out.println("驾驶员减速停止");
}
}
abstract class Kite implements Flyable{
@Override
public void fly() {
}
}
class Bullet extends Object implements Flyable,Attackable,CC{
@Override
public void attack() {
// TODO Auto-generated method stub
}
@Override
public void fly() {
// TODO Auto-generated method stub
}
@Override
public void stop() {
// TODO Auto-generated method stub
}
@Override
public void method1() {
// TODO Auto-generated method stub
}
@Override
public void method2() {
// TODO Auto-generated method stub
}
}
接口的基本使用总结
-
如果同时存在继承和接口的实现,定义
Java
类的语法格式: 先写extends
,后写implements
class SubClass extends SuperClass implements InterfaceA{ }
-
一个类可以实现多个接口, 接口也可以继承其它接口,弥补了
Java
单继承的局限性。 -
格式:
class AA extends BB implements CC,DD,EE
-
在
java
开发中,接口通过让类去实现(implements)的方式来使用。- 如果实现类覆盖了接口中的所有抽象方法,则此类就可以实例化
- 如果实现类没有覆盖接口中所有的抽象方法,则此实现类扔为一个抽象类
-
接口的主要用途就是被实现类实现(面向接口编程)。
-
接口和类是并列关系, 或者可以理解为一种特殊的类。 从本质上讲,接口是一种特殊的抽象类。
JDK7
以前:只能定义全局常量和抽象方法JDK8
:除了可以定义全局常量和抽象方法之外,还可以定义静态方法,默认方法。
-
与继承关系类似,接口与实现类之间存在多态性。
接口是一种规范
我们可以把 JAVA
中的接口比作我们日常生活中的USB接口
我们通过下面的一个例子,来更深入的了解接口的使用
public class USBTest {
public static void main(String[] args) {
Computer com = new Computer();
//1.创建了接口的非匿名实现类的非匿名对象
Flash flash = new Flash();
com.transferData(flash);
//2. 创建了接口的非匿名实现类的匿名对象
com.transferData(new Printer());
//3. 创建了接口的匿名实现类的非匿名对象
USB phone = new USB(){
@Override
public void start() {
System.out.println("手机开始工作");
}
@Override
public void stop() {
System.out.println("手机结束工作");
}
};
com.transferData(phone);
//4. 创建了接口的匿名实现类的匿名对象
com.transferData(new USB(){
@Override
public void start() {
System.out.println("mp3开始工作");
}
@Override
public void stop() {
System.out.println("mp3结束工作");
}
});
}
}
class Computer{
public void transferData(USB usb){//USB usb = new Flash();
usb.start();
System.out.println("具体传输数据的细节");
usb.stop();
}
}
interface USB{
//常量:定义了长、宽、最大最小的传输速度等
void start();
void stop();
}
class Flash implements USB{
@Override
public void start() {
System.out.println("U盘开启工作");
}
@Override
public void stop() {
System.out.println("U盘结束工作");
}
}
class Printer implements USB{
@Override
public void start() {
System.out.println("打印机开启工作");
}
@Override
public void stop() {
System.out.println("打印机结束工作");
}
}
上述的代码当中,需要在电脑(Computer)类中传输数据的闪存设备(Flash)需要实现(implements) USB
接口的规范,手机(Phone)、MP3设备使用 “匿名实现类“ 的方式来实现 USB
接口的规范。
接口的应用:代理模式
概述
代理模式是 Java
开发中使用较多的一种设计模式。代理设计就是为其他对象提供一种代理以控制对这个对象的访问。
举一个生活中的例子
比如大学生在毕业的时候需要去租房子,这是一类 “具体的功能“,
- 代理类:租房中介
- 被代理类:你自己,通过代理类(中介)去帮你找房源,找到合适的房源之后还得你自己去跟房东签合同
我们再来通过一个代码例子来进一步的理解什么是代理模式
/*
* 接口的应用:代理模式
*
*/
public class NetWorkTest {
public static void main(String[] args) {
BrowseGoogle browseGoogle = new BrowseGoogle();
BrowseFireFox browseFireFox = new BrowseFireFox();
//通过谷歌浏览器访问网络
ProxyBrowse proxyBrowseGoogle = new ProxyBrowse(browseGoogle);
proxyBrowseGoogle.browse();
//通过浏览器B访问网络
ProxyBrowse proxyBrowseFireFox = new ProxyBrowse(browseFireFox);
proxyBrowseFireFox.browse();
}
}
interface NetWork{
//浏览网页
public void browse();
}
//被代理类A:服务提供商
class BrowseGoogle implements NetWork{
@Override
public void browse() {
System.out.println("谷歌浏览器:正在访问网页");
}
}
//被代理类B:服务提供商
class BrowseFireFox implements NetWork{
@Override
public void browse() {
System.out.println("火狐浏览器:正在访问网页");
}
}
//代理类:需求方
class ProxyBrowse implements NetWork{
private NetWork work;
public ProxyBrowse(NetWork work){
this.work = work;
}
public void check(){
System.out.println("网上冲浪之前的检查工作");
}
public void end(){
System.out.println("关机,结束网上冲浪");
}
@Override
public void browse() {
this.check();
work.browse();
this.end();
}
}
运行结果如下
网上冲浪之前的检查工作
谷歌浏览器:正在访问网页
关机,结束网上冲浪
网上冲浪之前的检查工作
火狐浏览器:正在访问网页
关机,结束网上冲浪
在上面的代码当中,“代理类”(需求方)通过 “被代理类”(服务提供方)来实现浏览网页的行为。
应用场景:
- 安全代理: 屏蔽对真实角色的直接访问。
- 远程代理: 通过代理类处理远程方法调用(RMI)
- 延迟加载: 先加载轻量级的代理对象, 真正需要再加载真实对象
比如你要开发一个大文档查看软件, 大文档中有大的图片, 有可能一个图片有 100MB
, 在打开文件时, 不可能将所有的图片都显示出来,这样就可以使用代理模式,当需要查看图片时,用proxy来进行大图片的打开。
分类
- 静态代理(静态定义代理类)
- 动态代理(动态生成代理类)
- JDK自带的动态代理, 需要反射等知识
接口和抽象类之间的对比
在开发中,常看到一个类不是去继承一个已经实现好的类,而是要么继承抽象类,要么实现接口。
JDK1.8中接口的新特性
Java 8
中,你可以为接口添加静态方法和默认方法。从技术角度来说,这是完
全合法的,只是它看起来违反了接口作为一个抽象定义的理念。
-
静态方法: 使用 static 关键字修饰。 可以通过接口直接调用静态方法,并执行其方法体。我们经常在相互一起使用的类中使用静态方法。你可以在标准库中找到像
Collection/Collections
或者Path/Paths
这样成对的接口和类。Collection 为 jdk8 之后 Collections 的过渡版本
-
默认方法: 默认方法使用
default
关键字修饰。可以通过实现类对象来调用。我们在已有的接口中提供新方法的同时,还保持了与旧版本代码的兼容性。比如:
java 8 API
中对Collection
、List
、Comparator
等接口提供了丰富的默认方法。
通过下面的代码,我们来理解几个知识点
-
知识点1
接口中定义的静态方法,只能通过接口来调用。
-
知识点2
通过实现类的对象,可以调用接口中的默认方法。如果实现类重写了接口中的默认方法,调用时,仍然调用的是重写以后的方法
-
知识点3
如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的默认方法,那么子类在没有重写此方法的情况下,默认调用的是父类中的同名同参数的方法(类优先原则)
-
知识点4
如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,报错 (接口冲突)。这就需要我们必须在实现类中重写此方法
-
知识点5
如何在子类(或实现类)的方法中调用父类、接口中被重写的方法
public class SubClassTest {
public static void main(String[] args) {
SubClass s = new SubClass();
// s.method1();
// SubClass.method1();
//知识点1
CompareA.method1();
//知识点2
s.method2();
//知识点3、知识点4
s.method3();
}
}
class SubClass extends SuperClass implements CompareA,CompareB{
public void method2(){
System.out.println("SubClass:上海");
}
public void method3(){
System.out.println("SubClass:深圳");
}
//知识点5
public void myMethod(){
method3();//调用自己定义的重写的方法
super.method3();//调用的是父类中声明的
//调用接口中的默认方法
CompareA.super.method3();
CompareB.super.method3();
}
}
public class SuperClass {
public void method3(){
System.out.println("SuperClass:北京");
}
}
public interface CompareA {
//静态方法
public static void method1(){
System.out.println("CompareA:北京");
}
//默认方法
public default void method2(){
System.out.println("CompareA:上海");
}
default void method3(){
System.out.println("CompareA:上海");
}
}
public interface CompareB {
default void method3(){
System.out.println("CompareB:上海");
}
}
在jdk1.8之后,接口在保留原有的特性的同时,拥有了更多类的特征
接口新特性的应用举例
interface Filial {// 孝顺的
default void help() {
System.out.println("老妈,我来救你了");
}
}
interface Spoony {// 痴情的
default void help() {
System.out.println("媳妇,别怕,我来了");
}
}
class Father{
public void help(){
System.out.println("儿子,救我媳妇!");
}
}
class Man extends Father implements Filial, Spoony {
@Override
public void help() {
System.out.println("我该救谁呢?"); //自定义的
Filial.super.help(); //救你的老妈
Spoony.super.help(); //救媳妇
}
public static void main(String[] args) {
Man man = new Man();
man.help(); //如果Man未实现help方法,则默认调用父类的同名方法
}
}
0x07:内部类
当某一个类的内部,还需要有一部分需要以一个完整的结构进行描述时,而这部分结构又只为外部事物提供服务,那么这个结构最好使用 “内部类” 的结构进行定义。
- 在
java
中,如果类A
中如果还定义了 ”类B
“,那么后者类B
称为内部类,类A
成为外部类。 Inner class
一般用在定义它的类或者语句块之内,在外部引用它时必须给出完整的名称。Inner class
的名字不能与包含它的外部类的类名相同
内部类的分类:
- 成员内部类(
static
成员内部类和非static
成员内部类) - 局部内部类(不谈修饰符)、匿名内部类
成员内部类作为类的成员的角色:
- 和外部类不同,
Inner class
还可以声明为private
或protected
; - 可以调用外部类的结构
Inner class
可以声明为static
的, 但此时就不能再使用外层类的非static
的成员
变量
成员内部类作为类的角色:
-
可以在 内部定义属性、 方法、 构造器等结构
-
可以声明为
abstract
类 , 因此可以被其它的内部类继承 -
可以声明为
final
-
编译以后生成
OuterClass$InnerClass.class
字节码文件( 也适用于局部内部类)
通过例子来理解
public class Outer {
private int s = 111;
public class Inner {
private int s = 222;
public void mb(int s) {
System.out.println(s); // 局部变量s
System.out.println(this.s); // 内部类对象的属性s
System.out.println(Outer.this.s); // 外部类对象属性s
}
}
public static void main(String args[]) {
Outer a = new Outer();
Outer.Inner b = a.new Inner();
b.mb(333);
}
}
局部内部类
如何声明局部内部类 ?
class 外部类{
方法(){
class 局部内部类{
}
} {
class 局部内部类{
}
}
}
如何使用局部内部类 ?
-
只能在声明它的方法或代码块中使用,而且是先声明后使用。除此之外的任何地方
都不能使用该类 -
但是它的对象可以通过外部方法的返回值返回使用,返回值类型只能是局部内部类
的父类或父接口类型
public class InnerClassTest1 {
//开发中很少见
public void method(){
//局部内部类
class AA{
}
}
//返回一个实现了Comparable接口的类的对象
public Comparable getComparable(){
//创建一个实现了Comparable接口的类:局部内部类
//方式一:
// class MyComparable implements Comparable{
//
// @Override
// public int compareTo(Object o) {
// return 0;
// }
//
// }
//
// return new MyComparable();
//方式二:
return new Comparable(){
@Override
public int compareTo(Object o) {
return 0;
}
};
}
}
局部内部类的一些特点
- 内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的
.class
文件,但
是前面冠以外部类的类名和$
符号,以及数字编号。 - 只能在声明它的方法或代码块中使用,而且是先声明后使用。除此之外的任何地方
都不能使用该类。 - 局部内部类可以使用外部类的成员,包括私有的。
- 局部内部类可以使用外部方法的局部变量,但是必须是
final
的。 由局部内部类和局
部变量的声明周期不同所致。 - 局部内部类和局部变量地位类似,不能使用
public
、protected
缺省为private
- 局部内部类不能使用
static
修饰,因此也不能包含静态成员
匿名内部类
-
匿名内部类不能定义任何静态成员、方法和类,只能创建匿名内部类的一个实例。一个匿名内部类一定是在
new
的后面,用其隐含实现一个接口或实现一个类。 -
格式:
new 父类构造器(实参列表)|实现接口(){ //匿名内部类的类体部分 }
匿名内部类的特点
- 匿名内部类必须继承父类或实现接口
- 匿名内部类只能有一个对象
- 匿名内部类对象只能使用多态形式引用
interface A{
public abstract void fun1();
}
public class Outer{
public static void main(String[] args) {
new Outer().callInner(new A(){
//接口是不能new但此处比较特殊是子类对象实现接口,只不过没有为对象取名
public void fun1() {
System.out.println(“implement for fun1");
}
});// 两步写成一步了
}
public void callInner(A a) {
a.fun1();
}
}
}
总结
通过本次复习和整理,再次巩固了 JAVA
面向对象开发相关的知识点。
闲谈
学习过程中受到的一些启发
-
“比较少的人是真的把知识读通了,把知识是为他所用,而有一部分的人是学得越多越陷入进了知识的深渊,最终没办法走出来。”
-
每当学习一些新的知识时,给自己提出三个问题:What ? why? how?
是什么?为什么?怎么用?
-
"整理笔记,重要的是过程,而不是结果"