七、面向对象编程
7.1 怎么理解Java中的类和对象?
01、面向过程和面向对象
面向对象(Object-oriented programming,OOP):区别于面向过程思想,强调的是通过调用对象的行为来实现功能,而不是自己一步一步的去操作实现。它可以将复杂的事情简单化,并将我们从执行者变成了指挥者。面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
举个简单的例子来区分一下面向过程和面向对象:
假如有一天你想要吃大盘鸡了,这时候你会有两个选择:
- 自己买食材,包括:鸡、土豆、青椒等等,自己动手做;
- 到饭店去,只需要报个饭就好。
显然,第一种就是面向过程,第二种就是面向对象。
面向过程有什么劣势呢?假如你买好了食材准备去做一顿大盘鸡,但是呢,你临时又改变主意想要吃烩面了,这样的话,是不是还要重新去买食材。
而面向对象有什么优势呢?假如你不想吃大盘鸡了,你只需要对饭店的老板说一声:“大盘鸡不要了,换成烩面” 就好了。
这样对比着看会发现:
- 面向过程是
流程化
的,需要一步一步去做,只有等上一步做完了才可以去做下一步; - 面向对象是
模块化
的,你做你的我做我的,我需要你的话我会告诉你一声,但是我不需要知道你到底是怎么做的,也就是所谓的 “只看功劳不看苦劳” 了。
但是,如果追到底的话,面向对象的底层其实还是面向对象的,只不过把面向过程进行了抽象化,封装成了类,以方便我们的使用。
面向对象包含了三大基本特征,即封装、继承和多态。
区别:
面向过程:强调步骤;
面向对象:强调对象。
02、类和对象
对象可以是现实中任何看得见的物体,比如说:一个正在努力码代码的程序猿;也可以是想象中的任何虚拟物体,比如说:某动漫里的一个人物。
Java 通过类来定义这些物体,这些物体有什么状态,通过字段来定义,比如说:程序猿穿的衣服颜色是黑色的还是白色的;这些物体有什么行为,通过方法来定义,比如说:程序猿会码代码、会唱歌。
-
什么是类
类:是一组相关**属性(就是该事物的状态信息)和行为(就是该事物能够做什么)**的集合。可以看成是一类事物的模板,使用事物的属性特征和行为特征来描述该类事物。类是构造对象的模板或蓝图,由类构造对象的过程称为创建类的实例。
下面定义了一个简单的类:
/** * @author QHJ * @date 2022/8/26 09:52 * @description: 学生类 */ public class Student { private Long id; private String name; private int age; public Student(){ } public Student(Long id, String name, Integer age) { this.id = id; this.name = name; this.age = age; } private void eat(){ } private void sleep(){ } }
一个类可以包含:
- 字段(Filed)
- 方法(Method)
- 构造方法(Constructor)
在 Person 类中,字段有三个:id、name、age,它们也称为成员变量——在类内部但是在方法外部,在方法内部的叫做临时变量。
成员变量有时候也叫做实例变量,在编译时不占用内存空间,在运行时获取内存。也就是说,只有在对象实例化
(new Student())
后,字段才会获取到内存,这也正是它被称作 “实例” 变量的原因。这个类中有两个方法,分别是:eat()、sleep(),表示这个对象可以做什么。
public Student(){}
表示类的无参构造方法,因为是空的构造方法(方法体中没有内容),所以可以省略不写的。Java 的聪明劲儿就显示出来了,有些很死板的代码不需要开发人员添加,它会偷偷地做了。类之间最常见的关系有:
-
依赖(“use-a”)
如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。但是在开发中,应该尽可能地将相互依赖的类减至最少,也就是说
尽可能减少类之间的耦合
。 -
聚合(“has-a”)
类A的对象包含类B的对象。
-
继承(“is-a”)
类A继承类B。
-
什么是对象
对象:是一类事物的具体体现。对象是类的一个实例(对象并不是找个女朋友),必然具备该类事物的属性和行为。
类与对象的关系:
- 类是对一类事物的描述,是抽象的。
- 对象是一类事物的实例,是具体的。
- 类是对象的模板,对象是类的实体
对象的三个主要特征:
- 对象的行为(behavior):可以对对象完成哪些操作,或者可以对对象应用哪些方法?
- 对象的状态(state):当调用那些方法时,对象会如何响应?
- 对象的标识(identity):如何区分具有相同行为和状态的不同对象?
03、new 一个对象
Java 中使用 new
关键字来创建对象:
Student student = new Student();
通过 Student 类创建了一个 Student 对象。所有对象在创建时都会在堆内存中分配空间。
创建对象时需要一个 main()
方法作为入口,main() 方法可以在当前类中,也可以在另外一个类中。
在实际开发中,我们通常不在当前类中直接创建对象并使用它,而是放在使用对象的类中:
/**
* @author QHJ
* @date 2022/8/26 10:50
* @description: Student 对象
*/
public class StudentTest {
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.getName()); // null
System.out.println(student.getAge()); // 0
}
}
04、初始化对象
上面测试的输出结果为:null 0。这个结果是因为 Student 对象没有初始化(没有对字段赋值),所有输出了 String 的默认值 null、int 的默认值 0。
在 Java 中有三种方式初始化对象:
-
通过对象的引用变量
/** * @author QHJ * @date 2022/8/26 09:52 * @description: 学生类 */ public class Student { private Long id; private String name; private int age; public static void main(String[] args) { Student student = new Student(); student.id = 1L; student.name = "青花椒"; student.age = 22; System.out.println(student.id); // 1 System.out.println(student.name); // 青花椒 System.out.println(student.age); // 22 } }
student 被称作对象 Student 的引用变量
,通过对象的引用变量,可以直接对字段进行初始化:
-
通过方法初始化
自定义方法
initialize()
为变量赋值,然后在新建对象后调用该方法进行初始化:/** * @author QHJ * @date 2022/8/26 09:52 * @description: 学生类 */ public class Student { private Long id; private String name; private int age; public void initialize(Long i, String n, int a) { id = i; name = n; age = a; } public static void main(String[] args) { Student student = new Student(); // student.id = 1L; // student.name = "青花椒"; // student.age = 22; student.initialize(1L, "青花椒", 22); // System.out.println(student.id); // 1 System.out.println(student.name); // 青花椒 System.out.println(student.age); // 22 } }
-
通过构造方法初始化
这是最标准的一种做法,直接在 new 的时候把参数传递过去:
/** * @author QHJ * @date 2022/8/26 09:52 * @description: 学生类 */ public class Student { private Long id; private String name; private int age; public Student(Long id, String name, int age) { this.id = id; this.name = name; this.age = age; } public static void main(String[] args) { Student student = new Student(1L, "青花椒", 22); System.out.println(student.id); // 1 System.out.println(student.name); // 青花椒 System.out.println(student.age); // 22 } }
补充:
匿名对象,意味着没有引用变量,它只能在创建的时候被使用一次:
new Student();
可以直接通过匿名对象调用方法:
new Student().initialize(1L, "青花椒", 18);
05、对象的使用
//创建对象
类名 对象名 = new 类名();
//对象访问类中的成员
对象名.成员变量;
对象名.成员方法();
成员变量的默认值
数据类型 | 默认值 | |
---|---|---|
基本类型 | 整数(byte,short,int,long) | 0 |
浮点数(float,double) | 0.0 | |
字符(char) | ‘\u0000’ | |
布尔(boolean) | false | |
引用类型 | 类、对象、数组 | null |
public class testStudent {
public static void main(String[] args) {
//创建对象
Student s = new Student();
//直接输出成员变量值
System.out.println("姓名:"+s.name);
System.out.println("年龄:"+s.age);
//给成员变量赋值
s.name = "赵丽颖";
s.age = 12;
//再次输出成员变量的值
System.out.println("姓名:"+s.name);//赵丽颖
System.out.println("年龄:"+s.age);//12
//调用成员方法
s.study();
s.eat();
}
}
06、对象内存图
- 一个对象,调用一个方法内存图
Phone p 是存储在栈内存中的;
new Phone() 以及成员变量的赋值是存储在堆内存中的;
变量 p 指向堆内存中的空间,寻找方法信息,去执行该方法。 - 两个对象,调用同一方法内存图
Phone p(变量p是存储在栈内存中)指向堆内存中,系统作出方法标记,不做具体的操作;
Phone p2(变量p2是存储在栈内存中)指向堆内存中,系统作出方法标记,不做具体的操作;
方法信息在方法区中只保存一份,
根据不同变量拿到的方法标记的地址去方法区寻找方法并执行。 - 一个引用,作为参数传递到方法中内存图
引用类型作为参数,传递的是地址值。
07、关于对象
-
抽象的历程
所有编程语言都是一种抽象,甚至可以说,我们能够解决的问题的复杂程度取决于抽象的类型和质量。
Smalltalk 是历史上第一门获得成功的面向对象语言,也为 Java 提供了灵感。它有 5 个基本特征:
- 万物节对象;
- 一段程序实际上就是多个对象通过发送消息的方式来告诉彼此该做什么;
- 通过组合的方式,可以将多个对象封装成其他更为基础的对象;
- 对象是通过类实例化的;
- 同一类型的对象可以接收相同的消息。
总结一句话就是:
状态 + 行为 + 标识 = 对象
,每个对象在内存中都会有一个唯一的地址。 -
对象具有接口
所有的对象,都可以被归为一类,并且同一类对象拥有一些共同的行为和特征。在 Java 中,class 关键字用来定义一个类型。
创建抽象数据类型是面向对象编程的一个概念。你可以创建某种类型的变量,Java 中称之为对象或者实例,然后你就可以操作这些变量,Java 中称之为发送消息或者发送请求,最后对象决定自己该怎么做。
类描述了一系列具有相同特征和行为的对象。面向对象编程语言遇到的最大一个挑战就是,如何把现实/虚拟的元素抽象为 Java 中的对象。
对象能够接收什么样的请求是由它的接口定义的。具体是怎么做的就由它的实现方法来实现。
-
访问权限修饰符
类的创建者有时候也被称为 API 提供者,对应的,类的使用者就被称为 API 调用者。
JDK 就给我们提供了 Java 的基础实现——JDK 的作者也就是基础 API 的提供者(Java 多线程部分的作者 Doug Lea 是被 Java 程序员敬佩的一个大佬),我们这些 Java 语言的使用者——说白了就是 JDK 的调用者。
当然了,假如我们也提供了新的类给其他调用者,我们也就成为了新的创建者。
API 创建者在创建新的类的时候,只暴露必要的接口,而隐藏其他所有不必要的信息,之所以要这么做,是因为如果这些信息对调用者是不可见的,那么创建者就可以随意修改隐藏的信息,而不用担心对调用者的影响。
这里就引入了 Java 的权限修饰符。
访问权限修饰符的第一个作用是:
防止类的调用者接触到他们不该接触的内部实现
;第二个作用是:让类的创建者可以轻松修改内部机制而不用担心影响到调用者的使用
。 -
组合
我们可以把一个创建好的类作为另外一个类的成员变量来使用,利用已有的类组成一个新的类,被称为 “复用”,组合代表的关系时 has-a 的关系。
-
继承
继承是 Java 中非常重要的一个概念,子类继承父类,也就拥有了父类中 protected 和 public 修饰的方法和字段,同时,子类还可以扩展一些自己的方法和字段,也可以重写继承过来方法。
常见的例子,就是形状可以有子类圆形、方形、三角形,它们的基础接口是相同的,比如说都有一个 draw() 的方法,子类可以继承这个方法实现自己的绘制方法。
如果子类只是重写了父类的方法,那么它们之间的关系就是 is-a 的关系,但如果子类增加了新的方法,那么它们之间的关系就变成了 is-like-a 的关系。
-
多态
7.2 Java中的变量和常量
Java 变量就好像一个容器,可以保存程序在运行过程中的值,它在声明的时候会定义对应的数据类型(Java 的分为:基本数据类型和引用数据类型)。变量按照作用域的范围又可以分为:局部变量、成员变量和静态变量。
01、局部变量
在方法体内声明的变量被称为局部变量
,该变量只能在该方法内使用,类中的其他方法并不知道该变量。
/**
* @author QHJ
* @date 2022/8/29 10:31
* @description: 局部变量
*/
public class LocalVariable {
public static void main(String[] args) {
int age = 22;
String name = "青花椒";
int nowAge = age + 1;
System.out.println(name + nowAge);
}
}
其中,age、name、nowAge 就是局部变量,它们只能在当前这个 main() 方法中使用。
声明局部变量时的注意事项:
- 局部变量声明在方法中、构造方法或者语句块中;
- 局部变量在方法、构造方法、语句块被执行的时候创建,当他们执行完成后将会被销毁;
- 访问修饰符不能用于局部变量;
- 局部变量只在声明它的方法、构造方法或者语句块中可见;
- 局部变量是在栈上分配的;
- 局部变量没有默认值,所以局部变量被声明后,必须经过初始化才可以使用。
02、成员变量
在类内部、方法外部声明的变量
称为成员变量,或者实例变量(因为该变量只能通过类的实例来访问)。
/**
* @author QHJ
* @date 2022/8/29 10:43
* @description: 成员变量
*/
public class MemberVariable {
int data = 99;
private String name = "青花椒";
public static void main(String[] args) {
MemberVariable memberVariable = new MemberVariable();
System.out.println(memberVariable.data); // 99
System.out.println(memberVariable.name); // 青花椒
}
}
memberVariable 是一个引用类型的变量。new
关键字可以创建一个类的实例(也称为对象),通过 =
操作符赋值给 memberVariable 变量,memberVariable 就成了这个对象的引用,通过 memberVariable.data、memberVariable.name 就可以访问成员变量了。
声明成员变量时的注意事项:
- 成员变量声明在一个类中,但是在方法、构造方法和语句块之外;
- 当一个对象被实例化之后,每个成员变量的值就跟着确定了;
- 成员变量在对象创建的时候创建,在对象被销毁的时候销毁;
- 成员变量的值应该至少被一个方法、构造方法或语句块引用,使得外部能够通过这些方式获取实例变量信息;
- 成员变量可以声明在使用前或者使用后;
- 访问修饰符可以修饰成员变量;
- 成员变量对于类中的方法、构造方法或者语句块是可见的。
一般情况下应该把成员变量设为私有的。
通过使用访问修饰符可以使成员变量对子类可见;成员变量具有默认值。数值型变量的默认值是 0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定。
03、静态变量
通过 static 关键字声明的变量被称为静态变量(类变量),它可以直接被类访问。
/**
* @author QHJ
* @date 2022/8/29 11:12
* @description: 静态变量
*/
public class StaticVariable {
static int age = 22;
static String name = "青花椒";
public static void main(String[] args) {
System.out.println(StaticVariable.age); // 22
System.out.println(StaticVariable.name); // 青花椒
}
}
其中,age 和 name 就是静态变量,通过 类名.静态变量
就可以访问了,不需要创建类的实例。
声明静态变量时的注意事项:
- 静态变量在类中以 static 关键字声明,但必须在方法、构造方法或语句块之外;
- 无论一个类创建了多少个对象,类只拥有静态变量的一份拷贝;
- 静态变量存储在静态存储区;
- 静态变量在程序开始时创建,在程序结束时销毁;
- 与成员变量具有相似的可见性。但为了对类的使用者可见,大多数静态变量声明为 public 类型;
- 静态变量的默认值和实例变量相似;
- 静态变量还可以在静态语句块中初始化。
04、成员变量和局部变量的区别
- 在类中的位置不同
- 成员变量:类中,方法外
- 局部变量:方法中或者方法声明上(形式参数)
- 作用范围不一样
- 成员变量:类中
- 局部变量:方法中
- 初始化值的不同
- 成员变量:有默认值
- 局部变量:没有默认值。必须先定义,再赋值,才能使用
- 在内存中的位置不同
- 成员变量:堆内存
- 局部变量:栈内存
- 生命周期不同
- 成员变量:随着对象的创建而存在,随着对象的消失而消失
- 局部变量:随着方法的调用而存在,随着方法的调用完毕而消失
05、变量初始化
声明一个变量之后,必须用赋值语句对变量进行显式初始化。变量的声明尽可能地靠近变量第一次使用的地方。
从 Java 10 开始,对于局部变量,如果可以从变量的初始值推断出它的类型,就不需要声明类型,只需要使用关键字 var 而无需指定类型。
var greeting = "Hello";
var num = 12;
06、Java 中的常量
常量:是指在 java 程序中固定不变的数据,利用关键字 final
修饰的成员变量(一旦被赋值之后就不能再更改了)。习惯上,常量名使用全大写。
/**
* @author QHJ
* @date 2022/8/29 10:19
* @description: 常量
*/
public class FinalNumberTest {
final String NAME = "青花椒";
static final String DESC = "一枚有趣的程序猿";
public static void main(String[] args) {
FinalNumberTest test = new FinalNumberTest();
System.out.println(test.NAME); // 青花椒
System.out.println(DESC); // 一枚有趣的程序猿
}
}
常量在程序运行过程中主要有 2 个作用:
- 代表常数,便于修改(例如:圆周率的值,
final double PI = 3.14
); - 增强程序的可读性(例如:常量 UP、DOWN 用来代表上和下,
final int UP = 0
)。
常量的分类
类型 | 含义 | 数据举例 |
---|---|---|
整数常量 | 所有的整数 | 0,1, 567, -9 |
小数常量 | 所有的小数 | 0.0, -0.1, 2.55 |
字符常量 | 单引号引起来,只能写一个字符,必须有内容 | ‘a’ , ’ ', ‘好’ |
字符串常量 | 双引号引起来,可以写多个字符,也可以不写 | “A” ,“Hello” ,“你好” ,“” |
布尔常量 | 只有两个值(流程控制中) | true , false |
空常量 | 只有一个值(引用数据类型中) | null |
06、枚举类型
把变量的取值定义在一个集合内,在使用时直接调用,这个集合内的数据就叫做枚举类型。枚举类型包括有限个命名的值。
/**
* @author QHJ
* @date 2022/8/1 17:03
* @description: 自定义枚举类型
*/
public enum Size {
SMALL,
MEDIUM,
LARGE,
EXTRA_LARGE
}
/**
* @author QHJ
* @date 2022/8/1 17:05
* @description: 测试
*/
public class TypeTest {
public static void main(String[] args) {
// 使用枚举类型
Size size = Size.SMALL;
System.out.println(size);
}
}
7.3 Java中的方法
Java 中的方法用来实现代码的可重用性:编写一次方法,并多次使用它。通过增加或者删除方法中的一部分代码,就可以提高整体代码的可读性。
方法只有在被调用的时候才会执行。
其中最有名的方法就是 main()
方法了,这是程序的入口。
01、如何声明方法?
方法的声明反映了方法的一些信息,比如:可见性、返回类型、方法名和参数。
一个方法包含:方法头和方法体两部分。
访问权限:指定了方法的可见性。Java 提供了四种访问权限修饰符:
- public:该方法可以被所有类访问;
- private:该方法只能在定义它的类中访问;
- protected:该方法可以被同一个包中的类、或者不同包中的子类访问;
- default:该方法如果没有使用任何,Java 默认它使用 default 修饰符,该方法只能被同一个包中的类可见。
返回类型:方法返回的数据类型,可以是基本数据类型、对象和集合,如果不需要返回数据,就使用 void 关键字。
方法名:方法名最好反应出方法的功能,最好是一个动词,并且要以小写字母开头。如果方法名包含两个以上单词,第一个单词最好是动词,然后是形容词或者名词,并且要以驼峰式的命名方式命名:
- 一个单词:
sum()
- 多个单词:
stringComparable()
一个方法中可能与同一个类中的另外一个方法同名,这被称为方法重载。
参数:参数被放在一个圆括号内,如果有多个参数,可以使用逗号隔开。参数包含两部分:参数类型和参数名。如果方法没有参数,圆括号就是空的。
方法签名:每一个方法都有一个签名,包括方法名和参数。
方法体:方法体放在一对花括号内,把一些代码放在一起,用来执行特定的任务。
02、方法有哪几种?
方法可以分为两种:预先定义方法
和用户自定义方法
。
-
预先定义方法
Java 提供了大量预先定义好的方法供我们使用,也称为
标准类库方法
或者内置方法
。例如:String 类的 length()、equals()、compare() 方法,以及在控制台打印信息的 println() 方法等等。/** * @author QHJ * @date 2022/8/29 15:07 * @description: 预定义方法测试类 */ public class PredefinedMethodTest { public static void main(String[] args) { System.out.println("青花椒,一位有趣的程序猿"); } }
可以通过集成开发工具查看预先定义方法的方法签名,把鼠标停留在
println()
方法上面时:
由此可见,println()
方法的访问权限修饰符是 public,返回值类型是 void,方法名为 println,参数为 String x,以及 Javadoc(这个方法是干嘛的)。预先定义方法让编程变的更简单了,我们只需要在实现某些功能的时候调用这些方法即可,不需要重新编写。
-
用户自定义方法
当预先定义方法无法满足要求时,就需要自定义一些方法了。
/** * @author QHJ * @date 2022/8/29 15:18 * @description: 用户自定义方法及方法的调用 */ public class EvenOddTest { public static void main(String[] args) { findEvenOdd(26); // 26是偶数 findEvenOdd(51); // 51是奇数 } public static void findEvenOdd(int num){ if (num % 2 == 0){ System.out.println(num + "是偶数"); }else { System.out.println(num + "是奇数"); } } }
findEvenOdd()
方法用来检查输入的数字是奇数还是偶数。其中,方法名是 findEvenOdd,访问权限修饰符是 void,参数是一个 int 类型的 num,方法体中有一个 if else 语句。main()
方法是程序的入口,并且是静态的,所以可以直接调用静态方法findEvenOdd()
。当一个方法被 static 关键字修饰时,它就是一个静态方法。静态方法是属于类的,不属于类实例(不需要通过 new 关键字创建对象来调用,直接通过类名就可以调用)。
03、实例方法
没有使用 修饰,但在类中声明的方法被称为实例方法,在调用实例方法之前,必须创建类的对象:
/**
* @author QHJ
* @date 2022/8/29 15:33
* @description: 实例方法测试类
*/
public class InstanceMethodTest {
public static void main(String[] args) {
InstanceMethodTest test = new InstanceMethodTest();
System.out.println(test.add(4, 1)); // 5
}
public int add(int a, int b){
return a + b;
}
}
add()
方法是一个实例方法,需要创建 InstanceMethodTest 对象来访问。
实例方法有两种特殊类型:
- getter 方法:用来获取私有变量(private修饰的字段)的值。
- setter 方法:用来设置私有变量的值。
/**
* @author QHJ
* @date 2022/8/26 09:52
* @description: 学生类
*/
public class Student {
private Long id;
private String name;
private int age;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
getter 方法以 get 开头,setter 方法以 set 开头。
04、静态方法
/**
* @author QHJ
* @date 2022/8/29 15:41
* @description: 静态方法测试类
*/
public class StaticMethodTest {
public static void main(String[] args) {
System.out.println(add(1, 4)); // 5
}
public static int add(int a, int b){
return a + b;
}
}
StaticMethodTest 类中,main() 方法和 add() 方法都是静态的,不同的是:main() 方法是程序的入口。
当调用静态方法的时候就不需要 new 出来类的对象,而是可以直接调用静态方法了。一些工具类的方法都是静态方法,比如 ,里面有大量的静态方法可以直接调用。
Hutool 的目标是使用一个工具方法代替一段复杂代码,从而最大限度的避免"复制粘贴"代码的问题,彻底改变我们写代码的方式。
以计算MD5为例:
- ????【以前】打开搜索引擎 -> 搜“Java MD5加密” -> 打开某篇博客-> 复制粘贴 -> 改改好用
- ????【现在】引入Hutool -> SecureUtil.md5()
Hutool的存在就是为了减少代码搜索成本,避免网络上参差不齐的代码出现导致的bug。
05、抽象方法
没有方法体的方法被称为抽象方法
,它总是在抽象类中声明。这意味着如果类有抽象方法的话,这个类就必须是抽象的。可以使用 abstract
关键字创建抽象方法和抽象类:
/**
* @author QHJ
* @date 2022/8/29 16:05
* @description: 抽象类
*/
abstract class AbstractClassTest {
// 抽象方法
abstract void display();
}
/**
* @author QHJ
* @date 2022/8/29 16:06
* @description:
*/
public class MyAbstractClassTest extends AbstractClassTest{
@Override
void display() {
System.out.println("这个方法重写了抽象方法");
}
public static void main(String[] args) {
MyAbstractClassTest myAbstractClassTest = new MyAbstractClassTest();
myAbstractClassTest.display(); // 这个方法重写了抽象方法
}
}
7.4 Java中的构造方法
构造方法是一种特殊的方法,当一个类被实例化的时候,就会调用构造方法。只有在构造方法被调用的时候,对象才会被分配内存空间。每次使用 new 关键字创建对象的时候,构造方法至少会被调用一次。
如果在一个类中没有看见构造方法,并不是因为构造方法不存在,而是被缺省了,编译器会给这个类提供一个默认的构造方法。往大的方面说,就是 Java 有两种类型的构造方法:无参构造方法和有参构造方法
。
之所以叫它构造方法,是因为对象在创建的时候,需要通过构造方法初始化值——就是描写对象的那些状态(对应类中的字段)。
01、创建构造方法的规则
创造方法必须符合以下规则:
- 构造方法的名字必须和类名一样;
- 构造方法没有返回类型,包括 void;
- 构造方法不能是抽象的、静态的、最终的、同步的,也就是说,构造方法不能通过 abstract、static、final、synchronized 关键字修饰。
解析第三条规则:
- 由于构造方法不能被子类继承,所以用 final 和 abstract 修饰没有意义;
- 构造方法用于初始化一个对象,所以用 static 修饰没有意义;
- 多个线程不会同时创建内存地址相同的同一个对象,所以用 synchronized 修饰没有必要。
class class_name {
public class_name(){} // 默认无参构造方法
public ciass_name([paramList]){} // 定义有参数列表的构造方法
…
// 类主体
}
需要注意的是,如果用 void 声明构造方法的话,编译时是不会报错的,但是 Java 会把这个所谓的"构造方法"当成是普通方法来处理:
/**
* @author QHJ
* @date 2022/8/29 17:23
* @description: 构造方法测试类
*/
public class ConstructorTest {
public ConstructorTest() {
}
void ConstructorTest(){}
}
void ConstructorTest(){}
看起来很符合构造方法的写法(与类名相同),但其实只是一个不符合规范的普通方法:方法名的首字母使用了大写,方法体为空。但是它并不是默认的无参构造方法。
而 public ConstructorTest(){}
才是真正的无参构造方法。可以使用来修饰构造方法,访问权限修饰符决定了构造方法的创建方式。
构造方法虽然没有 返回值,但是返回的是类的对象。
02、什么是默认构造方法?
如果一个构造方法中没有任何参数,那么它就是一个默认构造方法,也称为无参构造方法:
/**
* @author QHJ
* @date 2022/8/29 17:42
* @description:
*/
public class Person {
public Person() {
System.out.println("这是一个无参构造方法的Person类");
}
public static void main(String[] args) {
Person person = new Person();
}
}
结果输出:这是一个无参构造方法的Person类
在 Person 类中,为 Person 类创建了一个无参的构造方法,它在创建对象的时候被调用。
通常情况下,无参构造方法是可以省略的,我们并不需要显式的声明无参构造方法,而是把这项工作交给了编译器:
默认构造方法的目的是什么呢?—— 默认构造方法的目的主要是为对象的字段提供默认值。
初始化字段只是构造方法的一种工作,它还可以做更多,比如启动线程、调用其他方法等等。
/**
* @author QHJ
* @date 2022/8/29 17:42
* @description: 默认构造方法
*/
public class Person {
private String name;
private int age;
public static void main(String[] args) {
Person person = new Person();
System.out.println("姓名:" + person.name + "年龄:" + person.age); // 姓名:null年龄:0
}
}
从上述代码中可以看到,默认构造方法初始化了 name 和 age 的值,name 是 String 类型则默认值为 null,age 是 int 类型所以默认值是 0.如果没有默认构造方法的话,这项工作就无法完成了。
03、什么是有参构造方法?
有参数的构造方法被称为有参构造方法,参数可以有一个或多个。有参构造方法可以为不同的对象提供不同的值或相同的值:
/**
* @author QHJ
* @date 2022/8/29 18:00
* @description: 有参构造方法
*/
public class ParamConstructorPerson {
private String name;
private int age;
public ParamConstructorPerson(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
ParamConstructorPerson p1 = new ParamConstructorPerson("青花椒", 22);
System.out.println("姓名:" + p1.name + " 年龄:" + p1.age); // 姓名:青花椒 年龄:22
ParamConstructorPerson p2 = new ParamConstructorPerson("青花椒", 18);
System.out.println("姓名:" + p2.name + " 年龄:" + p2.age); // 姓名:青花椒 年龄:18
}
}
由此可见,构造方法有两个参数:name 和 age。这样的话,我们在创建对象的时候就可以直接为 name 和 age 赋值了。
如果没有有参构造方法的话,就需要通过 setter 方法给字段赋值了。
04、如何重载构造方法?
在 Java 中,构造方法和方法类似,只不过没有返回类型。它也可以像方法一样被。构造方法的重载只需要提供不同的参数列表即可,编译器会通过参数的数量来决定应该调用哪一个构造方法:
/**
* @author QHJ
* @date 2022/8/29 18:12
* @description: 构造方法重载类
*/
public class OverloadingConstructorPerson {
private String name;
private int age;
private int sex;
public OverloadingConstructorPerson(String name, int age, int sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public OverloadingConstructorPerson(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
OverloadingConstructorPerson p1 = new OverloadingConstructorPerson("青花椒", 22, 1);
System.out.println("姓名:" + p1.name + " 年龄:" + p1.age + " 性别:" + p1.sex); // 姓名:青花椒 年龄:22 性别:1
OverloadingConstructorPerson p2 = new OverloadingConstructorPerson("青花椒", 22);
System.out.println("姓名:" + p2.name + " 年龄:" + p2.age); // 姓名:青花椒 年龄:22
}
}
在创建对象的时候,编译器会根据传入的参数判断要调用哪个构造方法。
05、构造方法和方法的区别
构造方法和方法之间的区别还是很多的(此图是盗用二哥的):
06、如何复制对象?
复制一个对象可以通过三种方式来完成:
- 通过构造方法
- 通过对象的值
- 通过 Object 类的 clone() 方法
-
通过构造方法
/** * @author QHJ * @date 2022/8/29 18:18 * @description: 复制对象(通过构造方法) */ public class CopyConstructorPerson { private String name; private int age; private int sex; public CopyConstructorPerson(String name, int age, int sex) { this.name = name; this.age = age; this.sex = sex; } public CopyConstructorPerson(CopyConstructorPerson person) { this.name = person.name; this.age = person.age; } public static void main(String[] args) { CopyConstructorPerson p1 = new CopyConstructorPerson("青花椒", 22, 1); System.out.println("姓名:" + p1.name + " 年龄:" + p1.age + " 性别:" + p1.sex); // 姓名:青花椒 年龄:22 性别:1 CopyConstructorPerson p2 = new CopyConstructorPerson(p1); System.out.println("姓名:" + p2.name + " 年龄:" + p2.age); // 姓名:青花椒 年龄:22 } }
由此可见,有一个参数为 CopyConstrutorPerson 的构造方法,可以把该参数的字段直接复制到新的对象中,这样的话,就可以在 new 关键字创建新对象的时候把之前的 p1 对象传递过去。
-
通过对象的值
/** * @author QHJ * @date 2022/8/29 18:22 * @description: 复制对象(通过对象的值) */ public class CopyValuePerson { private String name; private int age; public CopyValuePerson(String name, int age) { this.name = name; this.age = age; } public CopyValuePerson() { } public static void main(String[] args) { CopyValuePerson p1 = new CopyValuePerson("青花椒",22); System.out.println("姓名:" + p1.name + " 年龄:" + p1.age); // 姓名:青花椒 年龄:22 CopyValuePerson p2 = new CopyValuePerson(); p2.name = p1.name; p2.age = p1.age; System.out.println("姓名:" + p2.name + " 年龄:" + p2.age); // 姓名:青花椒 年龄:22 } }
这种方式是比较粗暴的,直接拿 p1 的字段值复制给 p2 对象(p2.name = p1.name)。
-
通过 Object 类的 clone() 方法
/** * @author QHJ * @date 2022/8/29 18:24 * @description: 复制对象(通过 clone() 方法) */ public class Cloneable implements java.lang.Cloneable { private String name; private int age; public Cloneable(String name, int age) { this.name = name; this.age = age; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public static void main(String[] args) throws CloneNotSupportedException { Cloneable p1 = new Cloneable("沉默王二",18); System.out.println("姓名:" + p1.name + " 年龄:" + p1.age); // 姓名:青花椒 年龄:22 Cloneable p2 = (Cloneable) p1.clone(); System.out.println("姓名:" + p2.name + " 年龄:" + p2.age); // 姓名:青花椒 年龄:22 } }
通过 clone() 方法复制对象的时候,ClonePerson 必须先实现 Cloneable 接口的 clone() 方法,然后再调用 clone() 方法(ClonePerson p2 = (ClonePerson) p1.clone())
7.5 Java中的代码初始化块
代码初始化块用于初始化一些成员变量,对象在创建的时候会执行代码初始化块。
可以直接通过 "="
操作符对成员变量进行初始化,但通过代码初始化块可以做更多的事情,比如:打印出成员变量初始化后的值。
我们可以直接通过 =
操作符对成员变量进行初始化:
/**
* @author QHJ
* @date 2022/8/30 10:58
* @description: 代码初始化块
*/
public class Bike {
int speed = 100;
}
代码初始化块是怎么使用的呢?看下面的代码:
/**
* @author QHJ
* @date 2022/8/30 10:58
* @description: 代码初始化块
*/
public class Bike {
List<String> list;
{
list = new ArrayList<>();
list.add("青花椒");
list.add("程序猿");
}
public static void main(String[] args) {
System.out.println(new Bike().list); // [青花椒, 程序猿]
}
}
如果只使用 =
操作符的话是没办法完成集合的初始化的:= 后面只能 new 出集合,却没办法填充值,而代码初始化就可以完成这项工作。
构造方法和代码初始化块谁执行的更早呢?
/**
* @author QHJ
* @date 2022/8/30 11:15
* @description: 代码初始化块
*/
public class Car {
Car() {
System.out.println("构造方法");
}
{
System.out.println("代码初始化块");
}
public static void main(String[] args) {
new Car(); // 代码初始化块 构造方法
}
}
从结果来看,好像是代码初始化块执行的更早。但是事实是这样吗?——不是的。
对象在初始化的时候会先调用构造方法,这是毫无疑问的,只不过构造方法在执行的时候会把代码初始化块放在构造方法中其他的代码之前,所以看到的先是 “代码初始化块”,后是 “构造方法”。
对于代码初始化来说,它有三个规则:
- 类实例化的时候执行代码初始化块;
- 实际上,代码初始化块是放在构造方法中执行的,只不过比较靠前;
- 代码初始化块里的执行顺序是从前到后的。
/**
* @author QHJ
* @date 2022/8/30 13:40
* @description: 父类
*/
class Father {
Father() {
System.out.println("父类构造方法");
}
}
/**
* @author QHJ
* @date 2022/8/30 13:40
* @description: 子类
*/
public class Child extends Father{
Child() {
System.out.println("子类构造方法");
}
{
System.out.println("代码初始化块");
}
public static void main(String[] args) {
new Child();
}
}
输出结果:
父类构造方法
代码初始化块
子类构造方法
在默认情况下,子类的构造方法在执行的时候会主动去调用父类的构造方法。也就是说,其实是构造方法先执行的,再执行的代码初始化块。
7.6 Java包可以优雅地解决类名冲突
01、package
在自己开发中,自己可以把类和接口命名为 Person、Student、Hello 等简单的名字。
在团队开发中,如果张三写了一个 Person 类,李四也写了一个 Person 类。现在王五想要用张三的 Person,同时也想用李四的 Person,该怎么办?
如果赵六写了一个 Arrays 类,但是呢 JDK 也自带了一个 Arrays 类,该如何解决类名冲突呢?
在 Java 中,我们使用 package 来解决名字的冲突。
Java 定义了一种名字空间,称之为包:package。一个类总是属于某个包的,类名只是一个简写,真正的完整类名是 包名.类名
。
在定义类的时候,我们需要在第一行声明这个类是属于哪个包的:
// 这是张三的 Person 类
package zhangsan;
public class Student{
}
// 这是李四的 Person 类
package lisi;
public class Student{
}
在 Java 虚拟机执行的时候,JVM 只看完整类名。因此,只要包名不同,类就不同。
包可以是多层结构,用 .
隔开。
要特别注意的是:包没有父子关系。
java.util 和 java.util.zip 是不同的包,两者没有任何继承关系。
没有定义包名的类,它使用的是默认包,非常容易引起名字冲突,因此不推荐不写包名的做法。
我们还需要按照包结构把上面的 Java 文件组织起来:
以 BaseJava 作为根目录,src 作为源码目录,所有 Java 文件对应的目录层次要和包的层次一致。
02、包作用域
位于同一个包的类,可以访问包作用域的字段和方法。
不用 public
、protected
、private
修饰的字段和方法就是包作用域,Student 类定义在 zhangsan 包下:
package com.qhj.zhangsan;
/**
* @author QHJ
* @date 2022/8/26 09:52
* @description: 学生类
*/
public class Student {
private Long id;
private String name;
private int age;
public Student(Long id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
// 包作用域
void eat() {
System.out.println("人要吃饭!");
}
}
StudentTest 类也定义在 zhangsan 包下,可以直接访问 Student 类:
package com.qhj.zhangsan;
/**
* @author QHJ
* @date 2022/8/26 10:50
* @description: Student 对象
*/
public class StudentTest {
public static void main(String[] args) {
Student student = new Student(1L, "张三的包", 23);
student.eat();
}
}
03、import
在一个类中,我们总会引用其他的类。例如:张三的 zhangsan.person
类,如果要引用赵六的 zhaoliu.Arrays
类,有三种写法:
-
直接写出完整类名
package com.qhj.zhangsan; /** * @author QHJ * @date 2022/8/26 10:50 * @description: Student 对象 */ public class StudentTest { public static void main(String[] args) { com.qhj.zhaoliu.Arrays arrays = new com.qhj.zhaoliu.Arrays(); } }
-
使用
import
语句导入package com.qhj.zhangsan; // 导入完整类名 import com.qhj.zhaoliu.Arrays; // 导入包下的所有 import com.qhj.zhaoliu.*; /** * @author QHJ * @date 2022/8/26 10:50 * @description: Student 对象 */ public class StudentTest { public static void main(String[] args) { Arrays arrays = new Arrays(); } }
在使用
import
导入的时候,可以选择导入完整类名,也可以使用*
,表示把这个包下面的所有类都导入进来(但是不包括子包的类)。但是一般不推荐这种写法,因为在导入了多个包后,会很难看出来Arrays
类属于哪个包。 -
使用
import static
的语法import static
可以导入一个类的静态字段和静态方法,但是一般很少使用:package com.qhj.zhangsan; // 导入 System 类的所有静态字段和方法 import static java.lang.System.*; /** * @author QHJ * @date 2022/8/26 10:50 * @description: Student 对象 */ public class StudentTest { public static void main(String[] args) { // 相当于调用了 System.out.println(); out.println("Hello world!"); } }
Java 编译器最终编译出的 .class 文件只使用完整类名,因此在代码中,当编译器遇到一个类名称时:
- 如果是完整类名,就直接根据完整类名查找这个类;
- 如果是简单类名,按照顺序以此查找:
① 查找当前 package 是否存在这个类
② 查找 import 的包是否包含这个类
③ 查找 java.lang 包是否包含这个类
如果按照这个规则还无法确定类名,则编译报错。
注意:
- 编写类的时候,编译器会自动帮我们做两个 import 动作:
① 默认自动 import 当前 package 的其他类
② 默认自动 import java.lang.* - 如果有两个类名称相同,例如:
com.qhj.Arrays
和java.util.Arrays
,那么只能 import 其中一个,另一个必须写完整的类名。
04、总结
-
最佳实践
为了避免名字冲突,我们需要确定唯一的包名。推荐的做法就是使用
倒置的域名来确保唯一性
,子包就可以根据功能自行命名:org.apache; org.apache.commons.log; com.qhj.*;
要注意不要和
java.lang
包的类重名,即自己的类不要使用这些名字:String. System. Runtime. ...
同时也不要和 JDK 常用类重名:
java.util.List; java.text.Format; java.math.BigInteger; ...
-
小结
- Java 内建的 package 机制是为了避免类名冲突;
- JDK 的核心类使用
java.lang
包,编译器会自动导入; - JDK 的其他常用类定义在
java.util.*
、java.math.*
、java.text.*
、…; - 包名推荐使用倒置的域名。