Java基础学习笔记

学习来源:尚硅谷

学习时间:2022年3月14日

1-3 基本语法

1.1 关键字与保留字

关键字

  • 关键字(keyword)的定义和特点
    • 定义:被Java语言赋予了特殊含义,用做专门用途的字符串(单词)
    • 特点:关键字中所有字母都为小写

image-20220314135002250

image-20220314135011809

保留字

Java保留字:现有Java版本尚未使用,但以后版本可能会作为关键字使用。自己命名标识符时要避免使用这些保留字,例如goto 、const

1.2 标识符

  • 标识符:
    • Java 对各种变量、方法和类等要素命名时使用的字符序列称为标识符
    • 技巧:凡是自己可以起名字的地方都叫标识符。

名称命名规范

  • 包名:多单词组成时所有字母都小写:xxxyyyzzz
  • 类名、接口名:多单词组成时,所有单词的首字母大写:XxxYyyZzz
  • 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写:xxxYyyZzz(驼峰命名法)
  • 常量名:所有字母都大写。多单词时每个单词用下划线连接:XXX_YYY_ZZZ

1.3 变量

  • 实质:内存中的一个存储区域

    • 该区域的数据可以在同一类型范围内不断变化
    • 变量是程序中最基本的存储单元。包含变量类型、变量名和存储的值
  • 变量作用域:其定义所在的一对{ }内,变量只有在其作用域内才有效,同一个作用域内,不能定义重名的变量

1.3.1 分类

  • 按数据类型分类:

image-20220314135502230

  • 按声明的位置的不同
    • 在方法体外,类体内声明的变量称为成员变量。
    • 在方法体内部声明的变量称为局部变量。
    • 异同:都有生命周期。局部变量除形参外,需显式初始化。

image-20220314135552285

1.3.2 整数类型

具体有:byteshortintlong

Java各整数类型有固定的表数范围和字段长度,不受具体OS的影响,以保证java程序的可移植性。

java的整型常量默认为 int 型,声明long型常量须后加lL

java程序中变量通常声明为int型,除非不足以表示较大的数,才使用long

类型 占用存储空间 表数范围
byte 1Byte = 8bit -128~127
short 2B -2^15^~2^15^-1
int 4B -2^31^~2^31^-1(大约21亿)
long 8B -2^63^~2^63^-1

1.3.3 浮点类型

具体有:floatdouble

与整数类型类似,Java 浮点类型也有固定的表数范围和字段长度,不受具体操作系统的影响。

  • 浮点型常量有两种表示形式:

    • 十进制数形式:如:5.12 512.0f .512 (必须有小数点)
    • 科学计数法形式:如:5.12e2 512E2 100E-2
  • float:单精度,尾数可以精确到7位有效数字。很多情况下,精度很难满足需求。

  • double:双精度,精度是float的两倍。通常采用此类型。

  • Java 的浮点型常量默认为double型,声明float型常量,须后加fF

类型 占用存储空间 表数范围
float 4B -3.403E38~3.403E38
double 8B -1.798E308~1.798E308

1.3.4 字符类型

char类型

  • char 型数据用来表示通常意义上“字符”(占用2字节)

  • Java中的所有字符都使用Unicode编码,故一个字符可以存储一个字母,一个汉字,或其他书面语的一个字符。

  • 字符型变量的三种表现形式:

    • 字符常量是用单引号(‘ ’)括起来的单个字符。例如:char c1 = 'a';char c2 = '中';char c3 = '9';

    • Java中还允许使用转义字符\(反斜杠)来将其后的字符转变为特殊字符型常量。例如:

      1
      char c3 = '\n'; // '\n'表示换行符
    • 直接使用 Unicode 值来表示字符型常量:\uXXXX。其中,XXXX代表一个十六进制整数。如:\u000a 表示 \n

  • char类型是可以进行运算的。因为它都对应有Unicode码。

Unicode编码

一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,使用 Unicode 没有乱码的问题。

Unicode 的缺点:Unicode 只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储:无法区别 Unicode 和 ASCII:计算机无法区分三个字节表示一个符号还是分别表示三个符号。另外,我们知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储空间来说是极大的浪费。

UTF-8

UTF-8 是在互联网上使用最广的一种 Unicode 的实现方式

UTF-8 是一种变长的编码方式。它可以使用 1-6 个字节表示一个符号,根据不同的符号而变化字节长度。

  • UTF-8的编码规则:
    • 对于单字节的UTF-8编码,该字节的最高位为0,其余7位用来对字符进行编码(等同于ASCII码)。
    • 对于多字节的UTF-8编码,如果编码包含 n 个字节,那么第一个字节的前 n 位为1,第一个字节的第 n+1 位为0,该字节的剩余各位用来对字符进行编码。在第一个字节之后的所有的字节,都是最高两位为”10”,其余6位用来对字符进行编码。

1.2.5 布尔类型

boolean类型,一般用于程序流程控制。

boolean类型数据只允许取值true和false,无null。

  • 不可以使用0或非0的整数替代false和true,这点和C语言不同。
  • Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达所操作的boolean值,在编译之后都使用java虚拟机中的int数据类型来代替:true用1表示,false用0表示

1.2.6 字符串类型

String不是基本数据类型,属于引用数据类型。

一个字符串可以串接另一个字符串,也可以直接串接其他类型的数据。

1.2.7 类型转换

① 基本数据类型转换

自动类型转换:容量小的类型自动转换为容量大的数据类型。数据类型按容量大小排序为:

image-20220314151310617

以下略

4 面向对象编程(上)

4.1 面向过程与面向对象

面向过程(Procedure Oriented Programming,POP)与面向对象(Object Oriented Programming,OOP):二者都是一种思想,面向对象是相对于面向过程而言的。

  • 面向过程,强调的是功能行为,以函数为最小单位,考虑怎么做。
  • 面向对象,将功能封装进对象,强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。

面向对象更加强调运用人类在日常的思维逻辑中采用的思想方法与原则,如抽象、分类、继承、聚合、多态等。

面向对象的三大特征EIP

  1. 封装 (Encapsulation)
  2. 继承 (Inheritance)
  3. 多态 (Polymorphism)

4.2 类和对象

4.2.1 概述

  • 类(Class)和对象(Object)是面向对象的核心概念。

    • 类是对一类事物的描述,是抽象的、概念上的定义
    • 对象是实际存在的该类事物的每个个体,因而也称为实例(instance)。
  • 类的成员:

    • 属性:对应类中的成员变量
    • 行为:对应类中的成员方法
    • Field = 属性 = 成员变量,Method = (成员)方法 = 函数
1
2
3
4
5
6
7
8
9
10
class Person {
// 属性,或成员变量
String name;
int age;
boolean isMarried;
// 方法,或函数
public void walk() {
System.out.println("Walk...");
}
}

4.2.2 类的语法格式

1
2
3
4
修饰符 class 类名 {
属性声明;
方法声明;
}

4.3 对象的创建与使用

4.3.1 概述和示例

  • 创建对象语法: 类名 对象名 = new 类名();

  • 使用对象名.对象成员的方式访问对象成员(包括属性和方法)

  • 如果创建了一个类的多个对象,对于类中定义的属性,每个对象都拥有各自的一套副本,且互不干扰。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class PersonTest {
public static void main(String[] args) {
Person person = new Person();
// 调用属性和方法
person.name = "Mark";
System.out.println(person.name);
System.out.println(person.age);
person.eat();
person.sleep();
person.talk("English");
}
}

class Person {
// 属性
String name;
int age = 1;
boolean isMale;

// 方法
public void eat() {
System.out.println("eat...");
}

public void sleep() {
System.out.println("sleep...");
}

public void talk(String language) {
System.out.println("talk: " + language);
}
}

4.3.2 内存解析

① JVM结构

image-20220416164008407

  • 堆(Heap),此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
  • 通常所说的栈(Stack),是指虚拟机栈。虚拟机栈用于存储局部变量等。局部变量表存放了编译期可知长度的各种基本数据类型(boolean、byte、char 、 short 、 int 、 float 、 long 、double)、对象引用(reference类型,它不等同于对象本身,是对象在堆内存的首地址)。 方法执行完,自动释放。
  • 方法区(Method Area),用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
② 分析
1
2
3
4
5
6
7
8
9
10
11
@Test
public void test() {
Person p1 = new Person();
p1.name = "Tom";
p1.isMale = true;
Person p2 = new Person();
System.out.println(p2.name); // null
Person p3 = p1;
p3.age = 10;
System.out.println(p1.age); // 10
}

内存结构图

image-20220416165024846

其中p1-3均为对象引用。

4.3.3 匿名对象

  • 我们也可以不定义对象的句柄,而直接调用这个对象的方法。这样的对象叫做匿名对象。例如:
1
new Person().sleep();
  • 使用情况:
    • 如果对一个对象只需要进行一次方法调用,那么就可以使用匿名对象。我们经常将匿名对象作为实参传递给一个方法调用。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class InstanceTest {
public static void main(String[] args) {
Phone p = new Phone();
System.out.println(p);
p.playGame();
p.sendMail();

// 匿名
new Phone().sendMail();
new Phone().price = 1999;
new Phone().showPrice(); // 0.0

// 匿名对象的使用
PhoneFactory mall = new PhoneFactory();
mall.show(new Phone());
}
}

class PhoneFactory {
public void show(Phone phone) {
phone.sendMail();
phone.playGame();
}
}

class Phone {
double price;

public void sendMail() {
System.out.println("send mail...");
}

public void playGame() {
System.out.println("play game...");
}

public void showPrice() {
System.out.println("price is: " + this.price);
}
}

4.4 类的属性

4.4.1 概述

语法格式

1
修饰符 数据类型 属性名 = 初始化值;
  • 修饰符:常用的权限修饰符有:private、缺省、protected、public,其他修饰符:static、final
  • 数据类型:任何基本数据类型(如int、Boolean) 或 任何引用数据类型。

变量分类

  • 在方法体外,类体内声明的变量称为成员变量
  • 在方法体内部声明的变量称为局部变量

image-20220416170007750

  • 局部变量除形参外,均需显式初始化。

  • 两者区别如下:

成员变量 局部变量
声明的位置 直接声明在类中 方法形参或内部、代码块内、构造器内等
修饰符 private、public、static、final等 不能用权限修饰符修饰,可以用final修饰
初始化值 有默认初始化值 没有默认初始化值,必须显式赋值,方可使用
内存加载位置 堆空间 或 静态域内(如果用static修饰) 栈空间

对象属性的默认初始化赋值

一个对象被创建时,会对其中各种类型的成员变量自动进行初始化赋值。除了基本数据类型之外的变量类型都是引用类型,如上面的Person及前面讲过的数组。

成员变量类型 初始值
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0
char 0 或写为:\u0000(表现为空)
boolean false
引用类型 null

4.4.2 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class UserTest {
public static void main(String[] args) {
User user = new User();
System.out.println(user.name); // null
System.out.println(user.age); // 0
System.out.println(user.isMale); // false
}
}

class User {
// 属性或成员变量
String name;
protected int age;
boolean isMale;

// 方法
public void talk(String language) { // 形参language,也是局部变量
System.out.println("talking: " + language);
}

public void eat() {
// 局部变量food
final String food = "川菜";
System.out.println("eat: " + food);
}
}

4.5 类的方法

4.5.1 概述

Java里的方法不能独立存在,所有的方法必须定义在类里。

方法的声明格式

1
2
3
4
修饰符 返回值类型 方法名(参数类型 形参1, 参数类型 形参2,...) {
方法体程序代码
return 返回值;
}

其中:

  • 修饰符:public,缺省,private,protected等
  • 返回值类型:
    • 没有返回值:void
    • 有返回值,声明出返回值的类型。与方法体中“return 返回值”搭配使用
  • 形参列表:可以包含零个,一个或多个参数。多个参数时,中间用“,”隔开

4.5.2 方法的重载

  • 重载(overload)的概念:在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可。“两同一不同”:同一个类,同一个方法名,不同的参数列表
    • 示例1:Arrays的sort方法
    • 示例2:System.out.println()
      • public void println(byte x)
      • public void println(short x)
      • public void println(int x)
      • public void println(long x)
      • public void println(float x)
      • public void println(double x)
      • public void println(char x)
      • public void println(double x)
      • public void println()
  • 重载的特点:与返回值类型无关,只看参数列表,且参数列表必须不同。(参数个数或参数类型)。调用时,根据方法参数列表的不同来区别。与权限修饰符、返回值类型、形参变量名和方法体都没有关系。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class OverloadTest {
public static void main(String[] args) {
OverloadTest test = new OverloadTest();
test.getSum(1, 1); // 1 2
}
// 以下都是重载的实例
public void getSum(int i, int j) {
System.out.println("1");
System.out.println(i + j);
}

public void getSum(double i, double j) {
System.out.println("2");
System.out.println(i + j);
}

public void getSum(int i, String s) {
System.out.println("3");
}

public void getSum(String s, int i) {
System.out.println("4");
}
}

4.5.3 方法参数的值传递机制

① 理解变量的赋值
  • 基本数据类型:赋值的是变量所保存的数据值
  • 引用数据类型:赋值的是变量保存的地址值

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ValueTransferTest {
public static void main(String[] args) {
// 基本数据类型
int m = 10;
int n = m;
System.out.println("m = " + m + ", n = " + n); // 10, 10
n = 20;
System.out.println("m = " + m + ", n = " + n); // 10, 20

// 引用数据类型
Order o1 = new Order();
o1.id = 1001;

Order o2 = o1; // 赋值以后,栈中的o1和o2的地址都指向堆中同一个对象
System.out.println("o1.id: " + o1.id + ", o2.id: " + o2.id); // 1001 1001

o2.id = 1002;
System.out.println("o1.id: " + o1.id + ", o2.id: " + o2.id); // 1002 1002
}
}

class Order {
int id;
}
② 值传递机制概述
  • 方法,必须由其所在类或对象调用才有意义。若方法含有参数:

    • 形参:方法声明时的参数
    • 实参:方法调用时实际传给形参的参数值
  • Java里方法的参数传递方式只有一种:值传递。 即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响。

    • 形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参
    • 形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参
③ 基本数据类型

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ValueTransferTest1 {
public static void main(String[] args) {
int m = 10;
int n = 20;
ValueTransferTest1 test = new ValueTransferTest1();
test.swap(m, n);
System.out.println("m = " + m + ", n = " + n); // 10 20
}

public void swap(int m, int n) {
int temp = m;
m = n;
n = temp;
System.out.println("m = " + m + ", n = " + n); // 20 10
}
}

内存结构图

image-20220422153600140

④ 引用数据类型

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ValueTransferTest2 {
public static void main(String[] args) {
Data data = new Data();
data.m = 10;
data.n = 20;
System.out.println("m = " + data.m + ", n = " + data.n); // 10 20
ValueTransferTest2 test = new ValueTransferTest2();
test.swap(data);
System.out.println("m = " + data.m + ", n = " + data.n); // 20 10
}

public void swap(Data data) {
int temp = data.m;
data.m = data.n;
data.n = temp;
}
}

class Data {
int m;
int n;
}

内存结构图

image-20220422154629770

4.6 封装与隐藏

4.6.1 概述

隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维护性。通俗的说,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。

Java中通过将数据声明为私有的(private),再提供公共的(public)方法:getXxx()setXxx()实现对该属性的操作,以实现下述目的:

  • 隐藏一个类中不需要对外提供的实现细节;
  • 使用者只能通过事先定制好的方法来访问数据,可以方便地加入控制逻辑,限制对属性的不合理操作;
  • 便于修改,增强代码的可维护性;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class AnimalTest {
public static void main(String[] args) {
Animal animal = new Animal();
// animal.name = "frog";
// animal.age = 24;
// animal.legs = 4;
// System.out.println(animal);

// animal.setLegs(-1); // 抛出异常
animal.setLegs(6);
System.out.println(animal.getLegs()); // 6
}
}

class Animal {
private String name;
private int age;
private int legs; // 将属性legs定义为private,只能被Animal类内部访问

// 属性设置
public void setLegs(int l) {
if (l >= 0 && l % 2 ==0) {
legs = l;
} else {
throw new RuntimeException("输入值错误!");
}
}

// 属性获取
public int getLegs() {
return legs;
}

public void eat() {
System.out.println("eat...");
}

@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
", age=" + age +
", legs=" + legs +
'}';
}
}

4.6.2 访问修饰符

  • Java权限修饰符publicprotected(缺省)private置于类的成员(属性、方法、构造器、内部类)定义前,用来限定对象对该类成员的访问权限

  • 对于class的权限修饰**只可以用public和default(缺省)**。

    • public类可以在任意地方被访问。
    • default类只可以被同一个包内部的类访问。
修饰符 类内部 同一个包 不同包的子类 同一个工程
private
缺省
protected
public

代码示例

  • Order.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Order {
private int orderPrivate;
int orderDefault;
public int orderPublic;

// 在类内部都可以使用
private void methodPrivate() {
orderPrivate = 1;
orderDefault = 2;
orderPublic = 3;
}

void methodDefault() {

}

public void methodPublic() {

}
}
  • OrderTest.java:位于同一个包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 与Order位于同一个包
public class OrderTest {
public static void main(String[] args) {
Order o = new Order();
o.orderDefault = 1;
o.orderPublic = 2;
// 'orderPrivate' has private access in 'com.hongyi.day11.Order'
// o.orderPrivate = 3;

o.methodDefault();
o.methodPublic();
// o.orderPrivate();
}
}
  • OrderTest.java:位于不同的包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 与Order位于不同的包
public class OrderTest {
public static void main(String[] args) {
Order o = new Order();
// 'orderDefault' is not public in 'com.hongyi.day11.Order'. Cannot be accessed from outside package
// o.orderDefault = 1;
o.orderPublic = 2;
// o.orderPrivate = 3;

// o.methodDefault();
o.methodPublic();
// o.orderPrivate();
}
}

4.7 构造器

4.7.1 概述

  • 构造器的特征

    • 它具有与类相同的名称
    • 它不声明返回值类型。(与声明为void不同)
    • 不能被static、final、synchronized、abstract、native修饰,不能有return语句返回值
  • 构造器的作用:

    • 创建对象
    • 给对象进行初始化
  • 语法格式

1
2
3
修饰符 类名 (参数列表) {
初始化语句;
}
  • 根据参数不同,构造器可以分为如下两类:
    • 隐式无参构造器(系统默认提供
    • 显式定义一个或多个构造器(无参、有参)
  • 注意:
    • Java语言中,每个类都至少有一个构造器
    • 默认构造器的修饰符与所属类的修饰符一致
    • 一旦显式定义了构造器,则系统不再提供默认构造器,此时需要自己提供一个无参构造器
    • 一个类可以创建多个重载的构造器
    • 父类的构造器不可被子类继承

4.7.2 代码示例

  • 空参构造器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class PersonTest {
public static void main(String[] args) {
// 创建类的实例
Person p = new Person(); // No Params Constructor...
}
}

class Person {
private String name;
private int age;

// 空参构造器
public Person() {
System.out.println("No Params Constructor...");
}

public void eat() {
System.out.println("eat...");
}

public void learn() {
System.out.println("learn");
}
}
  • 有参构造器和重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class PersonTest {
public static void main(String[] args) {
// 创建类的实例
Person p1 = new Person(); // No Params Constructor...
Person p2 = new Person("Hongyi");
Person p3 = new Person(24);
Person p4 = new Person("Mark", 24);
}
}

class Person {
private String name;
private int age;

// 空参构造器
public Person() {
System.out.println("No Params Constructor...");
}

// 多个有参构造器
public Person(String name) {
this.name = name;
}

public Person(int age) {
this.age = age;
}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public void eat() {
System.out.println("eat...");
}

public void learn() {
System.out.println("learn");
}
}

4.8 this关键字

4.8.1 概述

  • 在Java中,this关键字比较难理解,它的作用和其词义很接近。
    • 它在方法内部使用,即这个方法所属对象的引用;
    • 它在构造器内部使用,表示该构造器正在初始化的对象。
  • this 可以调用类的属性、方法和构造器
  • 什么时候使用this关键字呢?
    • 当在方法内需要用到调用该方法的对象时,就用this。

4.8.2 代码示例

代码示例1——this调用属性和方法

  • this理解为当前对象
  • 在任意方法或构造器内,如果使用当前类的成员变量或成员方法可以在其前面添加this,增强程序的阅读性。不过,通常我们都习惯省略this。
  • 形参与成员变量同名时,如果在方法内或构造器内需要使用成员变量,必须添加this来表明该变量是类的成员变量
  • 使用this访问属性和方法时,如果在本类中未找到,会从父类中查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class PersonTest1 {
public static void main(String[] args) {
Person1 p1 = new Person1();
p1.setName("Mark");
System.out.println(p1.getName()); // Mark
p1.eat();
}
}

class Person1 {
private String name;
private int age;

public Person1() {

}

public Person1(String name) {
// 不可以省略this
this.name = name;
}

public Person1(int a) {
// 可以省略this
age = a;
}

public void setName(String name) {
// this理解为当前对象
// 形参名和属性名相同时,必须使用this
this.name = name;
}

public String getName() {
return name;
}

public void setAge(int a) {
// 形参名和属性名不同,可以不使用this
age = a;
}

public int getAge() {
return age;
}

public void eat() {
System.out.println("eat...");
// 这里的this可以省略
this.speak();
}

public void speak() {
System.out.println("speak...");
}
}

代码示例2——this调用构造器

  • this可以作为一个类中构造器相互调用的特殊格式
  • 注意:
    • 可以在类的构造器中使用this(形参列表)的方式,调用本类中重载的其他的构造器!
    • 明确:构造器中不能通过this(形参列表)的方式调用自身构造器
    • 如果一个类中声明了n个构造器,则最多有 n - 1个构造器中使用了this(形参列表)
    • this(形参列表)必须声明在类的构造器的首行
    • 在类的一个构造器中,最多只能声明一个this(形参列表)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class PersonTest1 {
public static void main(String[] args) {
Person1 p1 = new Person1();
p1.setName("Mark");
System.out.println(p1.getName()); // Mark
p1.eat();
}
}

class Person1 {
private String name;
private int age;

public Person1() {
this.eat();
}

public Person1(String name) {
this(); // 调用空参构造器
this.name = name;
}

public Person1(String name, int age) {
this(name); // 调用上一个构造器
this.age = age;
}

public void setName(String name) {
// this理解为当前对象
// 形参名和属性名相同时,必须使用this
this.name = name;
}

public String getName() {
return name;
}

public void setAge(int a) {
// 形参名和属性名不同,可以不使用this
age = a;
}

public int getAge() {
return age;
}

public void eat() {
System.out.println("eat...");
// 这里的this可以省略
this.speak();
}

public void speak() {
System.out.println("speak...");
}
}

5 面向对象编程(中)

5.1 继承

5.1.1 概念

  • 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。此处的多个类称为子类(派生类),单独的这个类称为父类(基类或超类)。可以理解为:“子类 is a 父类”。

  • 类继承语法规则:

1
2
3
class Subclass extends SuperClass{
// ...
}
  • 作用:

    • 继承的出现减少了代码冗余,提高了代码的复用性。
    • 继承的出现,更有利于功能的扩展。
    • 继承的出现让类与类之间产生了关系,提供了多态的前提
  • 子类继承了父类,就继承了父类的方法和属性(私有的也算,但是封装性的影响使得子类不能直接调用父类的私有属性和方法,而可以通过父类提供的get或set方法操纵)。在子类中,可以使用父类中定义的方法和属性,也可以创建新的数据和方法。在Java 中,继承的关键字用是extends,即子类不是父类的子集,而是对父类的“扩展”。

  • 子类不能直接访问父类中私有的(private)的成员变量和方法。

  • Java只支持单继承和多层继承,不允许多重继承

    • 一个子类只能有一个父类
    • 一个父类可以派生出多个子类

image-20220424161202814

  • java.lang.Object类是所有类的直接或间接父类

代码示例

  • 父类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
@NoArgsConstructor
@AllArgsConstructor
public class People {
private String name;
private int age;

public void eat() {
System.out.println("eat...");
}

public void sleep() {
System.out.println("sleep...");
}
}
  • 子类
1
2
3
4
5
6
7
8
9
10
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student extends People{
private String major; // 定义自己的属性

public void learn() { // 定义自己的方法
System.out.println("learn...");
}
}
  • 测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ExtendsTest {
public static void main(String[] args) {
People p1 = new People();
p1.setAge(24);
p1.setName("Mark");
p1.eat();
p1.sleep();

Student s1 = new Student();
s1.setAge(24); // 使用父类中的属性和方法
s1.setName("Hongyi");
s1.setMajor("Computer Science");
s1.learn();
}
}

5.1.2 方法的重写

① 定义

重写(overwrite/override):在子类中可以根据需要对从父类中继承来的方法进行改造,也称为方法的重置、覆盖。在程序执行时,子类的方法将覆盖父类的方法

  • 要求
    • 子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表
    • 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型
    • 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限
      • 子类不能重写父类中声明为private权限的方法
    • 子类方法抛出的异常不能大于父类被重写方法的异常
  • 注意:子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。

代码示例

  • 父类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Person {
private String name;
private int age;

public Person() {

}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public void eat() {
System.out.println("eat...");
}

public void walk(int distance) {
System.out.println("walk: " + distance);
}

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;
}
}
  • 子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Student extends Person{
private String major;

public Student() {

}

public Student(String name, int age, String major) {
super(name, age);
this.major = major;
}

public void learn() {
System.out.println("learn: " + this.major);
}

// 对父类方法eat的重写
public void eat() {
System.out.println("Student eat...");
}
}
  • 测试类
1
2
3
4
5
6
7
8
public class PersonTest {
public static void main(String[] args) {
Student s = new Student("Mark", 24, "Computer Science");
s.eat(); // Student eat...
s.walk(10);
s.learn();
}
}
② 细节
  • 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限

    • public > 缺省 > protected > private
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Parent {
    public void method1() {

    }
    }
    class Child extends Parent {
    // 非法,子类中的method1()的访问权限private比被覆盖方法的访问权限public小
    private void method1() {

    }
    }
    • 子类不能重写父类中声明为private权限的方法
  • 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型

    • 父类为void,则子类必须是void
    • 父类返回值类型为A,则子类的返回值类型可以是A类,或者是A类的子类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Parent {
    public Object method1() {

    }
    }
    class Child extends Parent {
    // 合法
    public String method1() {

    }
    }
    • 父类返回值类型为基本数据类型,则子类必须为相同的基本数据类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Parent {
    public double method1() {

    }
    }
    class Child extends Parent {
    // 只能是double
    public double method1() {

    }
    }
  • 子类方法抛出的异常不能大于父类被重写方法的异常

5.1.3 super关键字

① 调用父类属性和方法
  • 在Java类中使用super来调用父类中的指定操作:

    • super可用于访问父类中定义的属性
    • super可用于调用父类中定义的成员方法
    • super可用于在子类构造器中调用父类的构造器
  • 注意:

    • 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
    • super的追溯不仅限于直接父类
    • super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识
    • 当子类重写父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式地使用super.方法的方式,表示调用的是父类中被重写的方法。

代码示例

  • 父类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Person {
String name;
int age;
int id = 1001; // 身份证号

public Person() {

}

public Person(String name) {
this.name = name;
}

public Person(int age) {
this.age = age;
}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public void eat() {
System.out.println("eat...");
}

}
  • 子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Student extends Person{
String major;
int id = 1002; // 学号

public Student() {

}

public Student(String major) {
this.major = major;
}

@Override
public void eat() {
System.out.println("Student eat...");
}

public void study() {
System.out.println("Student study...");
eat(); // 调用自身重写的方法
super.eat(); // 调用父类的方法
}

public void show() {
System.out.println("name = " + this.name + ", " + "age = " + super.age); // 子类没有这些属性,二者没有区别
System.out.println(id); // 子类id
System.out.println(super.id); // 父类id
}
}
  • 测试
1
2
3
4
5
6
public class SuperTest {
public static void main(String[] args) {
Student s = new Student();
s.show(); // 1002 1001
}
}
② 调用父类构造器
  • 子类中,在构造器的首行没有显式地声明this(形参)super(形参),则构造器默认都会访问父类中空参数的构造器super()
  • 当父类中没有空参数的构造器时,子类的构造器必须通过this(参数列表)或者super(参数列表)语句指定调用本类或者父类中相应的构造器。同时,只能”二选一”,且必须放在构造器的首行
  • 如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有无参的构造器,则编译出错。
  • 子类中至少有一个构造器调用了父类的空参构造器

代码示例

  • 子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Student() {
// super();
}

public Student(String major) {
// 默认super()会出现在子类的所有构造器的首行
// super();
this.major = major;
}

public Student(String name, int age, String major) {
// 调用父类构造器
super(name, age);
this.major = major;
}
  • 父类
1
2
3
4
// 空参构造器
public Person() {
System.out.println("I'm everywhere...");
}
  • 测试
1
2
3
4
5
6
7
8
9
public class SuperTest {
public static void main(String[] args) {
Student s1 = new Student("Mark", 24, "Computer Science");
s1.show(); // name = Mark, age = 24 1002 1001

Student s2 = new Student("Computer Science"); // I'm everywhere...
Student s3 = new Student(); // I'm everywhere...
}
}

5.1.4 子类对象的实例化过程

image-20220428202206302

5.2 多态

5.2.1 概念

多态性,是面向对象中最重要的概念,在Java中的体现:对象的多态性:父类的引用指向子类的对象(或子类的对象赋值给父类的引用),可以直接应用在抽象类和接口上。

Java引用变量有两个类型:编译时类型和运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。简称:编译时,看左边;运行时,看右边

  • 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)

  • 多态情况下:

    • “看左边”:看的是父类的引用(父类中不具备子类特有的方法),在编译期,只能调用父类中声明的方法。
    • “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法),在运行期,实际执行的是子类重写父类的方法。
    • 一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法
    1
    2
    3
    4
    Student m = new Student(); // 假设Student类有属性school
    m.school = “pku”; //合法,Student类有school成员变量
    Person e = new Student();
    e.school = “pku”; //非法,Person类没有school成员变量

    属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。但是内存中实际上是加载了子类特有的属性和方法

    • 对象的多态性只适用于方法,不适用于属性
  • 多态的使用前提:

    • 类之间存在继承的关系
    • 存在方法的重写

代码示例

  • 父类
1
2
3
4
5
6
7
8
9
10
11
12
public class Person {
String name;
int age;

public void eat() {
System.out.println("eat...");
}

public void walk() {
System.out.println("walk...");
}
}
  • 两个子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Man extends Person{
boolean isSmoking;

public void earnMoney() {
System.out.println("Man earn money");
}

@Override
public void eat() {
System.out.println("Man eat...");
}

@Override
public void walk() {
System.out.println("Man walk...");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Woman extends Person{
boolean isBeauty;

public void goShopping() {
System.out.println("Woman go shopping");
}

@Override
public void eat() {
System.out.println("Woman eat...");
}

@Override
public void walk() {
System.out.println("Woman walk...");
}
}
  • 测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class PersonTest {
public static void main(String[] args) {
Person p1 = new Person();
p1.eat(); // eat...

Man man = new Man();
man.eat(); // Man eat...
man.earnMoney(); // Man earn money

//------------------------------

// 对象的多态:父类的引用指向子类的对象
Person p2 = new Man();
Person p3 = new Woman();
// 多态的使用:当调用父子类同名同参数的方法时,实际执行的是子类重写父类的方法-->虚拟方法调用
p2.eat(); // Man eat...
p2.walk(); // Man walk...
// p2.earnMoney(); 不能调用Man中特有的方法,编译时p2为Person类型,没有Man类中的earnMoney方法
}
}

5.2.2 多态使用示例

方法声明的形参类型为父类类型,可以使用子类的对象作为实参调用该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class AnimalTest {
public static void main(String[] args) {
AnimalTest test = new AnimalTest();
test.func(new Dog()); // 传入的是Animal类型,实际执行的是Dog对象的方法
test.func(new Cat());
}

// 方法声明的形参类型为父类类型,可以使用子类的对象作为实参调用该方法
public void func(Animal animal) { // Animal animal = new Dog();
animal.eat();
animal.shout();
}
}

class Animal {
public void eat() {
System.out.println("Animal eat");
}

public void shout() {
System.out.println("animal shout");
}
}

// 继承
class Dog extends Animal {
// 重写
@Override
public void eat() {
System.out.println("Dog eat");
}

@Override
public void shout() {
System.out.println("Dog shout");
}
}

class Cat extends Animal {
@Override
public void eat() {
System.out.println("Cat eat");
}

@Override
public void shout() {
System.out.println("Cat shout");
}
}

5.2.3 虚拟方法调用

虚拟方法调用(多态情况下) :子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的,而是在运行时确定的(动态绑定)。

1
2
Person p = new Student();
p.getInfo(); // 调用的是Student类中的getInfo()方法

重载和重写的区别

重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。

Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。所以:对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”

而对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”

多态小结

  • 多态作用:提高了代码的通用性,常称作接口重用

  • 前提:

    • 需要存在继承或者实现关系
    • 有方法的重写
  • 成员方法:

    • 编译时:要查看引用变量所声明的类中是否有所调用的方法。
    • 运行时:调用实际new的对象所属的类中的重写方法。
  • 成员变量:

    • 不具备多态性,只看引用变量所声明的类

5.2.4 intanceof关键字

  • x instanceof A:检验x是否为类A的对象,返回值为boolean型。
    • 要求x所属的类与类A必须是子类和父类的关系,否则编译错误。
    • 如果x属于类A的子类B,x instanceof A值也为true。

代码示例

1
2
3
4
5
6
Person p1 = new Man();
if (p1 instanceof Man) {
Man m1 = (Man) p1;
m1.isSmoking = true;
m1.earnMoney();
}

5.2.5 对象类型转换

为了调用子类特有的属性和方法,需要进行对象类型转换(casting

  • 基本数据类型的Casting:
    • 自动类型转换:小的数据类型可以自动转换成大的数据类型,如long g=20; double d=12.0f
    • 强制类型转换:可以把大的数据类型强制转换(casting)成小的数据类型,如 float f=(float)12.0; int a=(int)1200L
  • 对Java对象的强制类型转换称为造型
    • 从子类到父类的类型转换可以自动进行
    • 从父类到子类的类型转换必须通过造型(强制类型转换)实现
    • 无继承关系的引用类型间的转换是非法的
    • 在造型前可以使用instanceof操作符测试一个对象的类型

image-20220429190111423

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PersonTest {
public static void main(String[] args) {
// 对象的多态:父类的引用指向子类的对象
Person p1 = new Man();
// 多态的使用:当调用父子类同名同参数的方法时,实际执行的是子类重写父类的方法--虚拟方法调用
p1.eat(); // Man eat...
p1.walk(); // Man walk...


// ------------------------------
// p2.earnMoney(); 不能调用Man中特有的方法
// p2.isSmoking = true; 不能调用Man中特有的属性
// 向下转型,使用强转时,可能出现ClassCastException异常
Man m1 = (Man) p1;
m1.isSmoking = true;
m1.earnMoney();
}
}

5.3 Object类

5.3.1 概述

Object类是所有Java类的根父类。

如果在类的声明中未使用extends关键字指明其父类,则默认父类为java.lang.Object类。

主要结构:

方法 类型 描述
1 public Object 构造器 构造器,且是唯一的构造器
2 public boolean equals(Object obj) 普通 对象比较
3 public int hashCode() 普通 取得Hash码
4 public String toString() 普通 对象打印时调用

5.3.2 ==和equals

  • ==
    • 基本类型比较值:只要两个变量的值相等,即为true。不一定类型相同。
    • 引用类型比较引用(是否指向同一个对象):只有指向同一个对象时(同一个地址),==才返回true。
    • 用“==”进行比较时,符号两边的数据类型必须兼容(可自动转换的基本数据类型除外),否则编译出错。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class EqualsTest {
public static void main(String[] args) {
// 基本数据类型比较
int i = 10;
int j = 10;
double d = 10.0;
boolean b = true;
char c = 10;
System.out.println(i == j); // true
System.out.println(i == d); // true
System.out.println(i == c); // true
// System.out.println(b == i); // 编译出错
char c1 = 'A';
char c2 = 65;
System.out.println(c1 == c2); // true

// 引用数据类型比较
Customer customer1 = new Customer("Mark", 24);
Customer customer2 = new Customer("Mark", 24);
System.out.println(customer1 == customer2); // false

String str1 = new String("Hongyi");
String str2 = new String("Hongyi");
System.out.println(str1 == str2); // false
}
}
  • equals:所有类都继承了Object,也就获得了equals()方法。还可以重写。

    • 只能比较引用类型,其作用与“==”相同,比较是否指向同一个对象。源码如下:
    1
    2
    3
    public boolean equals(Object obj) {
    return (this == obj); // 和==相同
    }
    • 格式:obj1.equals(obj2)
    • 特例:当用equals()方法进行比较时,对类File、String、Date及包装类(Wrapper Class)来说,是比较类型及内容而不考虑引用的是否是同一个对象;
      • 原因:在这些类中重写了Object类的equals()方法。
    • 当自定义使用equals()时,可以重写。用于比较两个对象的“内容”是否都相等。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class EqualsTest {
public static void main(String[] args) {

// 引用数据类型比较
Customer customer1 = new Customer("Mark", 24);
Customer customer2 = new Customer("Mark", 24);
System.out.println(customer1 == customer2); // false

String str1 = new String("Hongyi");
String str2 = new String("Hongyi");
System.out.println(str1 == str2); // false

System.out.println(customer1.equals(customer2)); // false
System.out.println(str1.equals(str2)); // true,String类重写了equals方法,只比较字符串内容
}
}

String类的equals方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}

重写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”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public boolean equals(Object obj) {
// 如果两个对象指向的地址相同,直接返回true
if (this == obj) {
return true;
}
if (obj instanceof Customer) {
// 向下转型
Customer customer = (Customer) obj;
// 比较对象的属性内容是否相同
return this.age == customer.age && this.name.equals(customer.name);
}
return false;
}

IDEA自动重写的equals方法:

1
2
3
4
5
6
7
8
9
10
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Customer customer = (Customer) o;

if (age != customer.age) return false;
return name.equals(customer.name);
}

高频面试题:==和equals的区别

  • == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址
  • equals的话,它是属于java.lang.Object类里面的方法,如果该方法没有被重写过默认也是==,我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals是比较值的错误观点。
  • 具体要看自定义类里有没有重写Object的equals方法来判断。
  • 通常情况下,重写equals方法,会比较类中的相应属性是否都相等。

5.3.3 toString方法

源码:很多类都重写toString方法。

1
2
3
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
  • toString()方法在Object类中定义,其返回值是String类型,返回类名和它的引用地址
1
2
3
4
Customer c1 = new Customer("Tom", 24);
System.out.println(c1); // com.hongyi.day17.Customer@5594a1b5
// 等价于
System.out.println(c1.toString());
  • 在进行String与其它类型数据的连接操作时,自动调用toString()方法。
1
2
3
Date now = new Date();
System.out.println(“now = ” + now); // 相当于
System.out.println(“now = ”+ now.toString());
  • 可以根据需要在用户自定义类型中重写toString()方法。如String 类重写了toString()方法,返回字符串的值。
1
2
s1 = “hello”;
System.out.println(s1);//相当于System.out.println(s1.toString());
  • 基本类型数据转换为String类型时,调用了对应包装类的toString()方法
1
2
int a=10; 
System.out.println(“a = ”+ a);

自定义类中IDEA重写的toString方法

1
2
3
4
5
6
7
@Override
public String toString() {
return "Customer{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

5.4 包装类

针对八种基本数据类型定义相应的引用类型—包装类(封装类,wrapper

有了类的特点,就可以调用类中的方法,Java才是真正的面向对象

image-20220429202505296

基本数据类型和包装类的转换

image-20220429203228404

  • 装箱:基本数据类型包装成包装类的实例

    • 通过包装类的构造器实现:
    1
    2
    int i = 500; 
    Integer t = new Integer(i);
    • 可以通过字符串参数构造包装类对象:
    1
    2
    Float f = new Float(“4.56”);
    Long l = new Long(“asdf”); //NumberFormatException
  • 拆箱:获得包装类对象中包装的基本类型变量

    • 调用包装类的.xxxValue()方法
    1
    boolean b = Obj.booleanValue();
  • JDK1.5之后,支持自动装箱,自动拆箱,但类型必须匹配。

1
2
Integer num1 = 10; // 自动装箱
int num2 = num1; // 自动拆箱

字符串与基本数据类型的转换

  • 字符串转换成基本数据类型

    • 通过包装类的构造器实现
    1
    int i = new Integer(“12”);
    • 通过包装类的parseXxx(String s)静态方法
    1
    Float f = Float.parseFloat(“12.1”);
  • 基本数据类型转换成字符串

    • 调用字符串重载的valueOf()方法
    1
    String fstr = String.valueOf(2.34f);
    • 更直接的方式
    1
    String intStr = 5 + "";

6 面向对象编程(下)

6.1 static关键字

6.1.1 设计思想

当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过new关键字才会产生出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。我们有时候希望无论是否产生了对象或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份,例如所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

类属性作为该类各个对象之间共享的变量。在设计类时,分析哪些属性不因对象的不同而改变,将这些属性设置为类属性。相应的方法设置为类方法

如果方法与调用者无关,则这样的方法通常被声明为类方法,由于不需要创建对象就可以调用类方法,从而简化了方法的调用。

6.1.2 使用

① 特点
  • 使用范围:在Java类中,可用static修饰属性、方法、代码块、内部类。

  • 被修饰后的成员具备以下特点:

    • 随着类的加载而加载

    • 优先于对象存在

    • 修饰的成员,被所有对象所共享

    • 访问权限允许时,可不创建对象,直接被类调用

    • 类只加载一次,因此静态变量在内存中只有一份,存在方法区的静态域中

② 静态变量

类变量(类属性)由该类的所有实例共享。

用static修饰属性:分为静态属性和非静态属性(实例变量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class StaticTest {
public static void main(String[] args) {
// 不用创建对象就可以访问静态成员
// 访问方式:类名.类属性,类名.类方法
Chinese.nation = "China";

Chinese c1 = new Chinese();
c1.name = "Hongyi";
c1.age = 24;

Chinese c2 = new Chinese();
c2.name = "Mark";
c2.age = 18;

System.out.println(c1.nation); // China
System.out.println(c2.nation);
}
}

// 中国人
class Chinese {
String name;
int age;
// 类变量
static String nation;
}

内存解析

image-20220430105606009

③ 静态方法
  • 没有对象的实例时,可以用类名.方法名()的形式访问由static修饰的类方法。
  • 在static方法内部只能访问类的static修饰的属性或方法,不能访问类的非static的结构。
  • 因为不需要实例就可以访问static方法,因此static方法内部不能有this和super
  • static修饰的方法不能被重写

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class StaticTest {
public static void main(String[] args) {
Chinese.show();
}
}

// 中国人
class Chinese {
String name;
int age;
// 类变量
static String nation;

public void eat() {
System.out.println("eat...");
}

// 静态方法
public static void show() {
// this.age = 24; 非法
// eat(); 非法
// 访问类的静态属性
System.out.println(nation);
System.out.println("show...");
}
}

6.1.3 单例设计模式

① 概念

设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模免去我们自己再思考和摸索。就像是经典的棋谱,不同的棋局,我们用不同的棋谱。

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为private这样,就不能用new操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的。

② 饿汉式

线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 {
// 2.内部创建类的对象
private static final Bank instance = new Bank();

// 1.私有化构造器
private Bank() {

}

// 3.提供公共的方法返回类的对象
public static Bank getInstance() {
return instance;
}
}
③ 懒汉式

延迟了对象的创建;具有线程并发的问题(线程不安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SingletonTest2 {
public static void main(String[] args) {
Order order1 = Order.getInstance();
Order order2 = Order.getInstance();
System.out.println(order1 == order2); // true
}
}

class Order {
// 1.私有化构造器
private Order() {

}

// 2.声明当前类对象,没有初始化
private static Order instance = null;

// 3.返回该对象
public static Order getInstance() {
if (instance == null) {
instance = new Order();
}
return instance;
}
}
④ 优点和应用场景

由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

应用场景

  1. 网站的计数器,一般也是单例模式实现,否则难以同步。
  2. 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  3. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
  4. 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,都生成一个对象去读取。
  5. Application 也是单例的典型应用
  6. Windows的Task Manager (任务管理器)就是很典型的单例模式
  7. Windows的Recycle Bin (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

6.1.4 理解main方法

  • Java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public
  • Java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static的,该方法接收一个String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数。
  • main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,这种情况,我们在之前的例子中多次碰到。

代码示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainTest {

public static void main(String[] args) { // 程序的入口
// show(); 报错,因为show不是静态方法
// 通过类的实例来调非静态方法
MainTest test = new MainTest();
test.show();
}

public void show() {
System.out.println("show...");
}
}

代码示例2

1
2
3
4
5
6
7
public class MainDemo {
public static void main(String[] args) {
for (String arg : args) {
System.out.println(arg);
}
}
}

image-20220430230714311

打印结果:

1
2
3
Mark
Hongyi
KisugiTakumi

6.2 代码块

代码块(或初始化块)的作用:对Java类或对象进行初始化。

代码块(或初始化块)的分类:一个类中代码块若有修饰符,则只能被static修饰,称为静态代码块(static block),没有使用static修饰的,为非静态代码块。

static代码块通常用于初始化static的属性:

1
2
3
4
5
6
7
class Person {
public static int total;
static {
total = 100; // 为total赋初值
}
//其它属性或方法声明
}
  • 静态代码块:用static 修饰的代码块
    • 可以有输出语句。
    • 可以对类的属性、类的声明进行初始化操作。
    • 不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法。
    • 若有多个静态的代码块,那么按照从上到下的顺序依次执行。
    • 静态代码块的执行要先于非静态代码块。
    • 静态代码块随着类的加载而加载,且只执行一次。
  • 非静态代码块:没有static修饰的代码块
    • 可以有输出语句。
    • 可以对类的属性、类的声明进行初始化操作。
    • 除了调用非静态的结构外,还可以调用静态的变量或方法。
    • 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
    • 每次创建对象的时候,都会执行一次。且先于构造器执行。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class BlockTest {
public static void main(String[] args) {
System.out.println(Person.desc);

Person p1 = new Person();
System.out.println(p1);
}
}

class Person {
String name;
int age;
static String desc = "I'm a human";

public Person() {

}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

// 静态代码块
static {
System.out.println("static block-1...");
desc = "I'm a human..."; // 重新对desc赋值
}

static {
System.out.println("static block-2...");
// 只能调用静态方法
info();
}

// 非静态代码块
{
// 创建对象时可以初始化属性
this.name = "Mark";
this.age = 24;
System.out.println("nonstatic block...");
}

{
System.out.println("nonstatic block-2...");
// 调用静态方法
info();
// 调用非静态方法
eat();
}

public void eat() {
System.out.println("eat...");
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

public static void info() {
System.out.println("info...");
}
}
1
2
3
4
5
6
7
8
9
static block-1...
static block-2...
info...
I'm a human...
nonstatic block...
nonstatic block-2...
info...
eat...
Person{name='Mark', age=24}

6.3 final关键字

6.3.1 概念

在Java中声明类、变量和方法时,可使用关键字final来修饰,表示“最终的”。

  • final标记的类不能被继承。提高安全性,提高程序的可读性。

    • String类、System类、StringBuffer类
  • final标记的方法不能被子类重写

    • Object类中的getClass()
  • final标记的变量(成员变量或局部变量)即称为常量。名称大写,且只能被赋值一次。

    • final标记的成员变量必须在声明时或在每个构造器中或代码块中显式赋值,然后才能使用。
    1
    final double MY_PI = 3.14;

6.3.2 修饰类

final标记的类不能被继承

1
2
3
4
5
6
7
8
9
10
11
12
final class FinalA {

}

// 报错
//class B extends FinalA {
//
//}

//class C extends String {
//
//}

6.3.3 修饰方法

final标记的方法不能被子类重写

1
2
3
4
5
6
7
8
9
10
11
12
class A {
public final void show() {

}
}

class B extends A {
// 不能重写父类的final方法
// public void show() {
//
// }
}

6.3.4 修饰变量

  • final修饰属性,可以考虑赋值的位置有:显式初始化、代码块中初始化和构造器中初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FinalTest1 {

final int WIDTH = 10; // 1.显式初始化
final int LEFT;
final int RIGHT;

public FinalTest1() {
RIGHT = 10; // 2.构造器初始化
}

public void doWidth() {
// WIDTH = 20; 报错,不能修改final修饰的变量
}

{
LEFT = 10; // 3.代码块初始化
}

public static void main(String[] args) {

}
}
  • final修饰局部变量:均不能被修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FinalTest2 {

public void show() {
final int NUM = 10; // 修饰常量
// NUM += 10; 编译不通过
}

public void show(final int num) { // 修饰形参
// num += 10; 编译不用过
System.out.println(num);
}

public static void main(String[] args) {
FinalTest2 test = new FinalTest2();
test.show(10);
}
}

6.4 抽象类与抽象方法

6.4.1 概念和使用

随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。

  • abstract关键字来修饰一个类,这个类叫做抽象类。抽象类中一定有构造器,便于子类实例化时调用。

  • abstract来修饰一个方法,该方法叫做抽象方法。

    • 抽象方法:只有方法的声明,没有方法的实现,以分号结束:
    1
    public abstract void talk();
  • 含有抽象方法的类必须被声明为抽象类。

  • 抽象类不能被实例化。抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。

  • 不能用abstract修饰变量、代码块、构造器;

  • 不能用abstract修饰:

    • 私有方法:抽象方法没有方法体,是用来被继承的,所以不能用private修饰
    • 静态方法:static修饰的方法可以通过类名来访问该方法(即该方法的方法体),抽象方法用static修饰没有意义
    • final的方法或类
    • 带有synchronized的方法

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class AbstractTest {
public static void main(String[] args) {
Student s = new Student();
s.eat();
}
}

// 抽象类
abstract class Person {
// 抽象方法
public abstract void eat();
}

class Student extends Person {

@Override
public void eat() {
System.out.println("Student eat...");
}
}

6.4.2 抽象类的匿名子类对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class HumanTest {
public static void main(String[] args) {
// 抽象类的匿名子类对象human
Human human = new Human() {
// 对抽象方法的重写
@Override
public void eat() {
System.out.println("eat");
}

@Override
public void breath() {
System.out.println("breath...");
}
};

human.eat();
human.breath();
}
}

// 抽象类
abstract class Human {

abstract public void eat();

abstract public void breath();
}

6.4.3 模板方法设计模式

抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。

解决的问题:当功能内部一部分实现是确定的,一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class TemplateTest {
public static void main(String[] args) {
Template t = new SubTemplate();
t.spendTime();
}
}

abstract class Template {
// 计算某段代码所花费的时间
public void spendTime() {
long start = System.currentTimeMillis();
code(); // 不确定的部分
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start));

}

public abstract void code();
}

class SubTemplate extends Template {

// 找1000以内的质数
@Override
public void code() {
for (int i = 2; i < 1000; i++) {
boolean flag = true;
for (int j = 2; j <= Math.sqrt(i); j++) {
if (i % j == 0) {
flag = false;
break;
}
}
if (flag) {
System.out.println(i);
}
}
}
}

6.5 接口

6.5.1 概念

一方面,有时必须从几个类中派生出一个子类,继承它们所有的属性和方法。但是,Java不支持多重继承。有了接口,就可以得到多重继承的效果。

另一方面,有时必须从几个类中抽取出一些共同的行为特征,而它们之间又没有is-a的关系,仅仅是具有相同的行为特征而已。例如:鼠标、键盘、打印机、扫描仪、摄像头、充电器、MP3机、手机、数码相机、移动硬盘等都支持USB连接。

接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要…则必须能…”的思想。继承是一个”是不是”的关系,而接口实现则是 “能不能”的关系。

接口的本质是契约,标准,规范,就像我们的法律一样。制定好后大家都要遵守。


  • 接口(interface)是抽象方法和常量值定义的集合。

  • 接口的特点:

    • interface来定义。
    • 接口中的所有成员变量都默认是由public static final修饰的。
    • 接口中的所有抽象方法都默认是由public abstract修饰的。
    • 接口中没有构造器。
    • 接口采用多继承机制。
    1
    2
    3
    4
    5
    6
    public interface Runner {
    int ID = 1;
    void start();
    public void run();
    void stop();
    }
  • 定义Java类的语法格式:先写extends,后写implements

  • 一个类可以实现多个接口,接口也可以继承其它接口。

  • 实现接口的类中必须提供接口中所有方法的具体实现内容,方可实例化。否则,仍为抽象类。

  • 接口的主要用途就是被实现类实现。(面向接口编程)

  • 与继承关系类似,接口与实现类之间存在多态性

  • 接口和类是并列关系,或者可以理解为一种特殊的类。从本质上讲,接口是一种特殊的抽象类

    • JDK7以前:只能定义全局常量和抽象方法,下列修饰符可以省略不写
      • 全局常量:public static final
      • 抽象方法:public abstract
    • JDK8:还可以定义静态方法、默认方法

6.5.2 使用

代码示例1——基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class InterfaceTest {
public static void main(String[] args) {
System.out.println(Flyable.MAX_SPEED);
System.out.println(Flyable.MIN_SPEED);

Plane plane = new Plane();
plane.fly();
plane.stop();
}
}

interface Flyable {
// 全局常量
int MAX_SPEED = 7000;
int MIN_SPEED = 1;

// 抽象方法
void fly();
void stop();
}

class Plane implements Flyable {

@Override
public void fly() {
System.out.println("Plane fly...");
}

@Override
public void stop() {
System.out.println("Plane stop...");
}
}

代码示例2——多实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class InterfaceTest {
public static void main(String[] args) {
Plane plane = new Plane();
plane.fly();
plane.stop();
plane.atta
}
}

interface Attack {
void attack();
}

interface Flyable {
// 全局常量
int MAX_SPEED = 7000;
int MIN_SPEED = 1;

// 抽象方法
void fly();
void stop();
}

// 先extends后implements
class Plane extends Object implements Flyable, Attack {

@Override
public void fly() {
System.out.println("Plane fly...");
}

@Override
public void stop() {
System.out.println("Plane stop...");
}

@Override
public void attack() {
System.out.println("Plane attack...");
}
}

代码示例3——接口的多态性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class USBTest {
public static void main(String[] args) {
Computer computer = new Computer();
Flash flash = new Flash();
computer.transferData(flash); // 接口的多态性 USB usb = new Flash();
}
}

class Computer {
public void transferData(USB usb) {
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("打印机结束工作");
}
}

代码示例4——创建接口匿名实现类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class USBTest {
public static void main(String[] args) {
Computer computer = new Computer();
// 1.创建了接口的非匿名实现类的非匿名对象
Flash flash = new Flash();
computer.transferData(flash); // 接口的多态性 USB usb = new Flash();

// 2.接口的非匿名实现类的匿名对象
computer.transferData(new Flash());

// 3.接口的匿名实现类的非匿名对象
USB phone = new USB() {
@Override
public void start() {
System.out.println("手机开始工作");
}

@Override
public void stop() {
System.out.println("手机结束工作");
}
};

computer.transferData(phone);

// 4.接口的匿名实现类的匿名对象
computer.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.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("打印机结束工作");
}
}

6.5.3 代理模式

代理模式(Proxy)是Java开发中使用较多的一种设计模式。代理设计就是为其他对象提供一种代理以控制对这个对象的访问。

  • 应用场景:

    • 安全代理:屏蔽对真实角色的直接访问。
    • 远程代理:通过代理类处理远程方法调用(RMI)
    • 延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象。比如你要开发一个大文档查看软件,大文档中有大的图片,有可能一个图片有100MB,在打开文件时,不可能将所有的图片都显示出来,这样就可以使用代理模式,当需要查看图片时,用proxy来进行大图片的打开。
  • 分类

    • 静态代理(静态定义代理类)
    • 动态代理(动态生成代理类):JDK自带的动态代理,需要反射等知识

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class NetworkTest {
public static void main(String[] args) {
Server server = new Server();
ProxyServer proxyServer = new ProxyServer(server);
proxyServer.browse();
}
}

interface Network {
public void browse();
}

// 被代理类
class Server implements Network {

@Override
public void browse() {
System.out.println("真实的服务器访问网络");
}
}

// 代理类
class ProxyServer implements Network {

private Network network;

public ProxyServer(Network network) {
this.network = network;
}

public void check() {
System.out.println("联网前的检查工作");
}

@Override
public void browse() {
check();
network.browse();
}
}
1
2
联网前的检查工作
真实的服务器访问网络

6.5.4 接口新特性

Java 8中,你可以为接口添加静态方法和默认方法。从技术角度来说,这是完全合法的,只是它看起来违反了接口作为一个抽象定义的理念。

静态方法:使用 static 关键字修饰。可以通过接口直接调用静态方法,并执行其方法体。我们经常在相互一起使用的类中使用静态方法。你可以在标准库中找到像Collection/Collections或者Path/Paths这样成对的接口和类。

默认方法:默认方法使用 default 关键字修饰。可以通过实现类对象来调用。我们在已有的接口中提供新方法的同时,还保持了与旧版本代码的兼容性。比如:java 8 API中对Collection、List、Comparator等接口提供了丰富的默认方法。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface CompareA {
static void method1() {
System.out.println("CompareA:static method");
}

// 默认方法
default void method2() {
System.out.println("CompareA:default method");
}

default void method3() {
System.out.println("CompareA:default method");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SubClassTest {
public static void main(String[] args) {
Subclass subclass = new Subclass();
// 调用默认方法,通过实现类对象来调用
subclass.method2();
subclass.method3();

// 调用静态方法,只能通过接口来调用
CompareA.method1();
}
}

class Subclass implements CompareA {
// 重写接口的默认方法
public void method3() {
System.out.println("Subclass:method3");
}
}

若一个接口中定义了一个默认方法,而另外一个接口中也定义了一个同名同参数的方法(不管此方法是否是默认方法),在实现类同时实现了这两个接口时,会出现:接口冲突。解决办法:实现类必须覆盖接口中同名同参数的方法,来解决冲突。

若一个接口中定义了一个默认方法,而父类中也定义了一个同名同参数的非抽象方法,则不会出现冲突问题。因为此时遵守:类优先原则。接口中具有相同名称和参数的默认方法会被忽略。

接口和抽象类的区别与联系

image-20220515162425141

6.6 内部类

6.6.1 概述

当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,那么整个内部的完整结构最好使用内部类。

在Java中,允许一个类的定义位于另一个类的内部,前者称为内部类,后者称为外部类。

内部类一般用在定义它的类或语句块之内,在外部引用它时必须给出完整的名称。

分类: 成员内部类(static成员内部类和非static成员内部类)和局部内部类(不谈修饰符)、匿名内部类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Person {

// 静态成员内部类
static class Dog {

}

// 非静态成员内部类
class Bird {

}

// 很少见
public void method() {
// 局部内部类
class AA {

}
}

{
// 局部内部类
class BB {

}
}

public Person() {
class CC {

}
}
}

6.6.2 成员内部类

  • 成员内部类作为类的成员的角色:
    • 和外部类不同,Inner class还可以声明为private或protected
    • 可以调用外部类的结构
    • Inner class 可以声明为static的,但此时就不能再使用外层类的非static的成员变量
  • 成员内部类作为类的角色:
    • 可以在内部定义属性、方法、构造器等结构
    • 可以声明为abstract类 ,因此可以被其它的内部类继承
    • 可以声明为final的,表示不能被继承
    • 编译以后生成OuterClass$InnerClass.class字节码文件(也适用于局部内部类)

注意

  1. 非static的成员内部类中的成员不能声明为static的,只有在外部类或static的成员内部类中才可声明static成员。
  2. 外部类访问成员内部类的成员,需要“内部类.成员”或“内部类对象.成员”的方式
  3. 成员内部类可以直接使用外部类的所有成员,包括私有的数据
  4. 当想要在外部类的静态成员部分使用内部类时,可以考虑内部类声明为静态的

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class InnerClassTest {
public static void main(String[] args) {
// 创建Dog的实例
Person.Dog dog = new Person.Dog();
dog.run();

// 创建Bird的实例
Person person = new Person();
Person.Bird bird = person.new Bird();
bird.sing();
bird.display("Hongyi");
}
}

class Person {

String name = "Mark";
int age;

public void eat() {
System.out.println("Person eat");
}

// 静态成员内部类,只能调用外部静态的结构
static class Dog {
String name;
public Dog() {

}
public void run() {
System.out.println("Dog run");
}
}

// 非静态成员内部类
class Bird {
String name = "bird";
public Bird() {

}
public void sing() {
System.out.println("Bird sing");
Person.this.eat(); // 调用外部类的属性(完整格式)
// eat();
}

public void display(String name) {
// 方法的形参
System.out.println(name);
// 调用自己的属性
System.out.println(this.name);
// 外部类的属性
System.out.println(Person.this.name);
}
}
}

6.6.3 局部内部类

内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号,以及数字编号。

只能在声明它的方法或代码块中使用,而且是先声明后使用。除此之外的任何地方都不能使用该类。

局部内部类可以使用外部类的成员,包括私有的。

局部内部类可以使用外部方法的局部变量,但是必须是final的。由局部内部类和局部变量的声明周期不同所致。

局部内部类和局部变量地位类似,不能使用public,protected,缺省,private。

局部内部类不能使用static修饰,因此也不能包含静态成员。

6.6.4 匿名内部类

匿名内部类不能定义任何静态成员、方法和类,只能创建匿名内部类的一个实例。一个匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类。

格式:

1
2
3
new 父类构造器(实参列表)|实现接口(){
// 匿名内部类的类体部分
}

匿名内部类的特点:

  • 匿名内部类必须继承父类或实现接口
  • 匿名内部类只能有一个对象
  • 匿名内部类对象只能使用多态形式引用

7 异常

7.1 概述

异常:在Java语言中,将程序执行中发生的不正常情况称为“异常”。 (开发过程中的语法错误和逻辑错误不是异常)

Java程序在执行过程中所发生的异常事件可分为两类:

  1. Error:Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。比如:StackOverflowErrorOOM。一般不编写针对性的代码进行处理。

  2. Exception: 其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如空指针访问,试图读取不存在的文件,网络连接中断
    ,数组角标越界等。

Error示例

  • 栈溢出
1
2
3
4
5
public class Test01 {
public static void main(String[] args) {
main(args);
}
}
1
2
3
4
5
6
Exception in thread "main" java.lang.StackOverflowError
at com.hongyi.day1.Test01.main(Test01.java:10)
at com.hongyi.day1.Test01.main(Test01.java:10)
at com.hongyi.day1.Test01.main(Test01.java:10)
at com.hongyi.day1.Test01.main(Test01.java:10)
...
  • 堆溢出(内存溢出)
1
2
3
4
5
public class Test01 {
public static void main(String[] args) {
Integer[] arr = new Integer[1024*1024*1024];
}
}
1
2
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.hongyi.day1.Test01.main(Test01.java:10)

对于这些错误,一般有两种解决方法:一是遇到错误就终止程序的运行。另一种方法是由程序员在编写程序时,就考虑到错误的检测、错误消息的提示,以及错误的处理。

捕获错误最理想的是在编译期间,但有的错误只有在运行时才会发生。比如:除数为0,数组下标越界等。

image-20220322192742749

注:蓝色:非受检(unchecked)异常(运行时异常);红色:受检(checked)异常(编译时异常)

  • 运行时异常:是指编译器不要求强制处置的异常。一般是指编程时的逻辑错误,是程序员应该积极避免其出现的异常。java.lang.RuntimeException类及它的子类都是运行时异常。
  • 编译时异常:是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。编译器要求Java程序必须捕获或声明所有编译时异常。对于这类异常,如果程序不处理,可能会带来意想不到的结果。编译时异常需要我们进行处理。

7.2 异常处理机制

7.2.1 概述

在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行x/y运算时,要检测分母为0,数据为空,输入的不是数据而是字符等。过多的if-else分支会导致程序的代码加长、臃肿,可读性差。因此采用异常处理机制。

Java采用的异常处理机制,是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁、优雅,并易于维护。

Java异常处理的方式:

  • try-catch-finally
  • throws + 异常类型

7.2.2 try-catch-finally

Java提供的是异常处理的抛抓模型

Java程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象将被提交给Java运行时系统,这个过程称为抛出(throw)异常。

异常对象的生成

  • 由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,如果在当
    前代码中没有找到相应的处理程序,就会在后台自动创建一个对应异常类的实例
    对象并抛出——自动抛出。
  • 由开发人员手动创建:创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样
1
Exception exception = new ClassCastException();
① 抛抓机制

image-20220322193709537

  • 抛:出现异常,就会在出现异常的代码处生成一个异常对象,并将此对象抛出,一旦抛出异常后,其后的代码就不会执行。
  • 抓:可以理解为处理异常的方式,有两种方式。

如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常。 如果一个异常回到main()方法,并且main()也不处理,则程序运行终止。

程序员通常只能处理Exception,而对Error无能为力。

② tcf处理机制

异常处理()是通过try-catch-finally语句实现的。

1
2
3
4
5
6
7
8
9
try{
...... //可能产生异常的代码
} catch( ExceptionName1 e ){
...... // 当产生ExceptionName1型异常时的处置措施
} catch( ExceptionName2 e ){
...... // 当产生ExceptionName2型异常时的处置措施
}[ finally{
...... // 可选,无论是否发生异常,都无条件执行的语句
}]
  • try:捕获异常的第一步是用try{…}语句块选定捕获异常的范围,将可能出现异常的代码放在try语句块中。
  • catch (ExceptionType e):在catch语句块中是对异常对象进行处理的代码。每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。如果明确知道产生的是何种异常,可以用该异常类作为catch的参数;也可以用其父类作为catch的参数。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test1() {
String str = "123";
str = "abc";
try {
int num = Integer.parseInt(str);
// 异常抛出后,后面的代码不执行
System.out.println("hello-----1"); // 不输出
} catch (NumberFormatException e) {
// e.getMessage();
e.printStackTrace();
}
// 注意,num声明于try块内,外面访问不到
// System.out.println(num);
System.out.println("hello-----2"); // 输出
}
  • 捕获异常的有关信息:与其它对象一样,可以访问一个异常对象的成员变量或调用它的方法。
    • getMessage() 获取异常信息,返回字符串
    • printStackTrace() 获取异常类名和异常信息,以及异常出现在程序中的位置。返回值void。

image-20220322200008957

  • finally:捕获异常的最后一步是通过finally语句为异常处理提供一个统一的出口,使得在控制流转到程序的其它部分以前,能够对程序的状态作统一的管理。
    • 不论在try代码块中是否发生了异常事件,catch语句是否执行,catch语句是否有异常,catch语句中是否有return,finally块中的语句都会被执行
    • 像数据库连接、输入输出流、网络编程Socket等资源,JVM不能自动回收的,需要我们手动释放资源,此时释放资源的语句就需要声明在finally中。
    • finally语句和catch语句是任选的

image-20220322200700958

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test02 {
public static void main(String[] args) {
FileInputStream fis = null;
try{
File file = new File("hello.txt");
fis = new FileInputStream(file);
int data = fis.read();
while (data != -1) {
System.out.println((char) data);
data = fis.read();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 释放资源
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
③ 不捕获异常时的情况

前面使用的异常都是RuntimeException类或是它的子类,这些类的异常的特点是:即使没有使用try和catch捕获,Java自己也能捕获,并且编译通过( 但运行时会发生异常使得程序运行终止 )。

如果抛出的异常是IOException等类型的非运行时异常,则必须捕获,否则编译错误。也就是说,我们必须处理编译时异常,将异常进行捕捉,转化为运行时异常。

7.2.3 throws

① 使用方式

声明抛出异常是Java中处理异常的第二种方式。写在方法的声明处。

  • 如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理而由该方法的调用者负责处理
  • 在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Test01 {
// main调用m2,捕获异常并处理
// main调用m3,因为m3已做了异常捕获和处理,因此不做异常处理
public static void main(String[] args) {
try {
m2();
} catch (IOException e) {
e.printStackTrace();
}
m3();
}

// m3调用m1,做捕获异常并异常处理
public static void m3() {
try {
m1();
} catch (IOException e) {
e.printStackTrace();
}
}

// m2调用m1,也将异常抛出,但不做异常处理
public static void m2() throws IOException {
m1();
}

// m1将异常抛出给调用者,不做异常处理
public static void m1() throws IOException {
File file = new File("hello.txt");
FileInputStream fis = new FileInputStream(file);
int data = fis.read();
while (data != -1) {
System.out.println((char) data);
data = fis.read();
}
fis.read();
System.out.println("Hello World");
}
}

总结

  • tcf机制真正地将异常处理掉了。
  • throws只是将异常抛给了方法的调用者,并没有将异常处理掉。
② 重写方法声明抛出异常的原则
  • 重写方法不能抛出比被重写方法范围更大的异常类型。在多态的情况下, 对methodA()方法的调用-异常的捕获按父类声明的异常处理。
  • 子类重写的方法抛出的异常类型不能大于父类被重写的方法抛出的异常类型

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SuperClass {
public void method() throws IOException{

}
}

class SubClass1 extends SuperClass{
public void method() throws FileNotFoundException {

}
}

class SubClass2 extends SuperClass{
// 报错
public void method() throws Exception {

}
}
③ 如何选择异常处理机制
  • 如果父类中被重写的方法没有throws方式处理异常,则子类重写的方法也不能使用throws,意味着子类中的方法中有异常,则必须使用tcf方式处理异常

  • 执行的方法a中,先后又调用了另外的几个方法,这几个方法是递进关系执行的,建议这几个方法使用throws的方式进行处理,而方法a采用tcf方式进行处理。

7.3 手动抛出异常

  • Java异常类对象除在程序执行过程中出现异常时由系统自动生成并抛出,也可根据需要使用人工创建并抛出。

    • 首先要生成异常类对象,然后通过throw语句实现抛出操作(提交给Java运行环境)。
    1
    2
    IOException e = new IOException();
    throw e;
    • 可以抛出的异常必须是Throwable或其子类的实例。下面的语句在编译时将会产生语法错误:
    1
    throw new String("want to throw");

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class StudentTest {
public static void main(String[] args) {
try {
Student s = new Student();
s.registry(-1);
System.out.println(s);
} catch (Exception e) {
// e.printStackTrace();
System.out.println(e.getMessage());
}
}
}

class Student {
private int id;
public void registry(int id) throws Exception {
if (id > 0) {
this.id = id;
} else {
// System.out.println("Error!");
// 手动抛出异常
throw new Exception("输入数据非法");
}
}

@Override
public String toString() {
return "Student{" +
"id=" + id +
'}';
}
}
1
输入数据非法

7.4 自定义异常类

  • 一般地,用户自定义异常类都是RuntimeException的子类。
    • 自定义异常类通常需要编写几个重载的构造器。
    • 自定义异常需要提供serialVersionUID
    • 自定义的异常通过throw手动抛出

代码示例

  • MyException.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1.继承现有的异常体系结构RuntimeException、Exception
public class MyException extends RuntimeException{

// 2.提供全局常量serialVersionUID
static final long serialVersionUID = -7034897190745766939L;

// 3.重载的构造器
public MyException() {

}

public MyException(String msg) {
super(msg);
}
}
  • 使用:Student.java
1
2
3
4
5
6
7
8
9
10
11
12
class Student {
private int id;
public void registry(int id) {
if (id > 0) {
this.id = id;
} else {
// System.out.println("Error!");
// 手动抛出异常
throw new MyException("输入数据非法");
}
}
}

7.5 练习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ReturnExceptionDemo {
static void methodA() {
try {
System.out.println("进入方法A");
throw new RuntimeException("制造异常");
}finally {
System.out.println("用A方法的finally");
}
}
static void methodB() {
try {
System.out.println("进入方法B");
return;
} finally {
System.out.println("调用B方法的finally");
}
}
public static void main(String[] args) {
try {
methodA();
} catch (Exception e) {
System.out.println(e.getMessage());
}
methodB();
}
}
1
2
3
4
5
进入方法A
用A方法的finally
制造异常
进入方法B
调用B方法的finally