1 类和对象

1.1 类的定义

  1. 是用来描述同一类事物的
  2. 可以在内部定义任意数量的、不同类型的变量,作为这一类事物的属性。这种属性叫做成员变量 ( member variable )。
  3. 如果一个Java文件中定义了一个public类,则文件名必须与该类的名称相同,包括大小写。
  4. 如果一个Java文件中定义了多个类,则只能有一个public类,该类的名称必须与文件名相同。
  5. 就好像文件路径+文件名不能重复一样,一个Java程序中相同名字的类只能有一个
1
2
3
4
5
6
7
8
9
10
11
12
13
// >> TODO 一个类以public class开头,public class代表这个类是公共类,类名必须和文件名相同。
// >> TODO public class后面紧跟类名,然后是一对打括号的类体
public class Merchandise {
// >> TODO 类体中可以定义描述这个类的属性的变量。我们称之为成员变量(member variable)
// >> TODO 每个成员变量的定义以;结束
String name;
String id;
int count;
double price;

}

// >> TODO 上面这整个类,其实就是创建了一个模版。描述了一种我们需要的数据类型。

1.2 匿名对象

可以不定义对象的句柄,而直接调用这个对象的方法,如下:new Person().shout();
使用情况:

  • 如果一个对象只需要执行一次方法调用,那么就可以使用匿名对象
  • 将匿名对象作为实参传递给一个方法调用

1.3 引用类型

Java中的数据类型分为基本数据类型和引用数据类型

  • 引用数据类型和基本数据类型的相同点
  1. 都可以用来创建变量,可以赋值和使用其值
  2. 本身都是一个地址
  • 引用数据类型和基本数据类型的不同点
  1. 基本类型变量的值,就是地址对应的值。引用数据类型的值还是一个地址,需要通过“二级跳”找到实例
  2. 引用数据类型是Java的一种内部类型,是对所有自定义类型和数组引用的统称,并非特指某种类型
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
public class Merchandise {
String name;
String id;
int count;
double price;

}
public class Merchandise {
String name;
String id;
int count;
double price;
}

public class ReferenceAndPrimaryDataType {
public static void main(String[] args) {

// >> TODO m1是一个Merchandise类型的引用,只能指向Merchandise类型的实例
// >> TODO 引用数据类型变量包含两部分信息:类型和实例。也就是说,
// TODO 每一个引用数据类型的变量(简称引用),都是指向某个类( class /自定义类型)
// TODO 的一个实例/对象(instance / object)。不同类型的引用在Java的世界里都是引用。
// >> TODO 引用的类型信息在创建时就已经确定,可以通过给引用赋值,让其指向不同的实例.
// 比如 m1 就是Merchandise类型,只能指向Merchandise的实例。
Merchandise m1;
m1 = new Merchandise();
Merchandise m2 = new Merchandise();
Merchandise m3 = new Merchandise();
Merchandise m4 = new Merchandise();
Merchandise m5 = new Merchandise();

// >> TODO 给一个引用赋值,则两者的类型必须一样。m5可以给m1赋值,因为他们类型是一样的
m1 = m5;

System.out.println("m1=" + m1);
System.out.println("m2=" + m2);
System.out.println("m3=" + m3);
System.out.println("m4=" + m4);
System.out.println("m5=" + m5);

Merchandise m6 = m1;
System.out.println("m6=" + m6);
m6 = m5;
System.out.println("m6=" + m6);


System.out.println("m1=" + m1);
System.out.println("m2=" + m2);
System.out.println("m3=" + m3);
System.out.println("m4=" + m4);
System.out.println("m5=" + m5);


int a = 999;

}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
m1=Merchandise@1b6d3586
m2=Merchandise@4554617c
m3=Merchandise@74a14482
m4=Merchandise@1540e19d
m5=Merchandise@1b6d3586
m6=Merchandise@1b6d3586
m6=Merchandise@1b6d3586
m1=Merchandise@1b6d3586
m2=Merchandise@4554617c
m3=Merchandise@74a14482
m4=Merchandise@1540e19d
m5=Merchandise@1b6d3586

以上的m1 == m5 == m6,引用的地址一样。

1.4 类对象和引用的关系

  • 类和对象的关系
  1. 类是对象的模版,对象是类的一个实例
  2. 一个Java程序中类名相同的类只能有一个,也就是类型不会重名
  3. 一个类可以有很多对象
  4. 一个对象只能根据一个类来创建
  • 引用和类以及对象的关系
  1. 引用必须是、只能是一个类的引用
  2. 引用只能指向其所属的类型的类的对象
  3. 相同类型的引用之间可以赋值
  4. 只能通过指向一个对象的引用,来操作一个对象,比如访问某个成员变量
  • 数组是一种特殊的类
  1. 数组的类名就是类型带上中括号
  2. 同一类型的数组,每个数组对象的大小可以不一样,也就是每个数组对象占用的内存可以不一样,这点和类的对象不同。
  3. 可以用引用指向类型相同大小不同的数组,因为他们属于同一种类型
  • 引用数组
  1. 可以把类名当成自定义类型,定义引用的数组,甚至多维数组
  • 示例1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class ArrayIsClass {
    public static void main(String[] args) {
    // >> TODO “数组变量”其背后真身就是引用。数组类型就是一种特殊的类。
    // >> TODO 数组的大小不决定数组的类型,数组的类型是只是由元素类型决定的。
    int[] intArr;
    intArr = new int[1];
    intArr = new int[2];

    // 这个数组的元素就是二维的double数组,既double[][]
    double[][][] double3DArray = new double[2][3][4];

    int[] a1 = new int[9];
    int[] a2 = new int[0];
    a2 = a1;
    System.out.println("a2.length=" + a2.length);
    double[] a3 = new double[5];
    // a3是double[]类型的引用,不可以用int[]类型的引用赋值。
    double3DArray[1][2] = a3;
    }
    }

    输出:

    1
    a2.length=9
  • 示例2

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

public static void main(String[] args) {
Merchandise[] merchandises = new Merchandise[9];
merchandises[0] = new Merchandise();
merchandises[1] = new Merchandise();
merchandises[0].name = "笔记本";
System.out.println(merchandises[0].name);

System.out.println(merchandises[2]);
}
}

输出:

1
2
笔记本
null

1.5 对象的内存解析

1.5.1 JVM内存结构划分

HotSpot Java虚拟机的架构图如下。其中主要关心的是运行时数据区部分(Runtime Data Area)。
image.png

堆:此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
栈:是指虚拟机栈。虚拟机栈用于存储局部变量等。
方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

1.5.2 对象内存解析

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person { //类:人 String name;
int age;
boolean isMale;
}

public class PersonTest { //测试类
public static void main(String[] args) {
Person p1 = new Person(); p1.name = "赵同学";
p1.age = 20;
p1.isMale = true;

Person p2 = new Person();
p2.age = 10;

Person p3 = p1;
p3.name = "郭同学";
}
}

内存解析图:
image.png

  • 堆:凡是new出来的结构(对象、数组)都放在堆空间中。
  • 对象的属性存放在堆空间中
  • 创建一个类的多个对象(比如:p1、p2),则每个对象都拥有当前类的一套“副本”(即属性)。当通过一个对象修改其属性时,不会影响其他对象此属性的值
  • 当声明一个新的变量使用现有的对象进行赋值时(比如p3=p1),两个变量会共同指向堆空间中同一个对象

1.6 类方法

1.6.1 构造方法

  1. 构造方法(constructor)的方法名必须与类名一样,而且构造方法没有返回值。这样的方法才是构造方法。
  2. 构造方法可以有参数,规则和语法于普通方法一样。使用时,参数传递给 new 语句后类名的括号后面。
  3. 如果没有显示的添加一个构造方法,Java会给每个类都会默认自带一个无参数的构造方法。
  4. 如果我们自己添加类构造方法,Java就不会再添加无参数的构造方法。这时候,就不能直接 new 一个对象不传递参数了(看例子)
  5. 所以我们一直都在使用构造方法,这也是为什么创建对象的时候类名后面要有一个括号的原因。
  6. 构造方法无法被点操作符调用或者在普通方法里调用,只能通过 new 语句在创建对象的时候,间接调用。
  7. 理解一下为什么构造方法不能有返回值,因为有返回值也没有意义,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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.geekbang.supermarket;

public class MerchandiseV2 {

public String name;
public String id;
// >> TODO 构造方法执行前,会执行给局部变量赋初始值的操作
// >> TODO 我们说过,所有的代码都必须在方法里,那么这种给成员变赋初始值的代码在哪个方法里?怎么看不到呢?
// TODO 原来构造方法在内部变成了<init>方法。学习就是要脑洞大,敢想敢试,刨根问底。
public int count = 999;// 999/0; 在构造方法前赋值变量
public double soldPrice;
public double purchasePrice;

// >> TODO 构造方法(constructor)的重载和普通方法一样
public MerchandiseV2(String name, String id, int count, double soldPrice, double purchasePrice) {
this.name = name;
this.id = id;
this.count = count;
this.soldPrice = soldPrice;
this.purchasePrice = purchasePrice;
// soldPrice = 9/0;
}

// >> TODO 在构造方法里才能调用重载的构造方法。语法为this(实参列表)
// >> TODO 构造方法不能自己调用自己,这会是一个死循环
// >> TODO 在调用重载的构造方法时,不可以使用成员变量。因为用语意上讲,这个对象还没有被初始化完成,处于中间状态。
// >> TODO 在构造方法里才能调用重载的构造方法时,必须是方法的第一行。后面可以继续有代码
public MerchandiseV2(String name, String id, int count, double soldPrice) {
// double purPrice = soldPrice * 0.8;
// this(name, id, count, soldPrice, purchasePrice);
this(name, id, count, soldPrice, soldPrice * 0.8);
// double purPrice = soldPrice * 0.8;
}

// >> TODO 因为我们添加了构造方法之后,Java就不会再添加无参数的构造方法。如果需要的话,我们可以自己添加这样的构造方法
public MerchandiseV2() {
this("无名", "000", 0, 1, 1.1);

}

public void describe() {
System.out.println("商品名字叫做" + name + ",id是" + id + "。 商品售价是" + soldPrice
+ "。商品进价是" + purchasePrice + "。商品库存量是" + count +
"。销售一个的毛利润是" + (soldPrice - purchasePrice));
}

public double buy(int count) {
if (this.count < count) {
return -1;
}
return this.count -= count;
}
}

  • 在构造方法里才能调用重载的构造方法。语法为this(实参列表)
  • 构造方法不能自己调用自己,这会是一个死循环
  • 在调用重载的构造方法时,不可以使用成员变量。因为用语意上讲,这个对象还没有被初始化完成,处于中间状态。
  • 在构造方法里才能调用重载的构造方法时,必须是方法的第一行。后面可以继续有代码

1.6.2 静态变量和静态方法

  1. 静态变量
  • 静态变量使用 static 修饰符
  • 静态变量如果不赋值,Java也会给它赋以其类型的初始值
  • 静态变量一般使用全大写字母加下划线分割。这是一个习惯用法
  • 所有的代码都可以使用静态变量,只要根据防范控制符的规范,这个静态变量对其可见即可
  • public 的静态变量,所有的代码都可以使用它
  • 如果没有public修饰符,只能当前包的代码能使用它
1
2
public static double DISCOUNT_FOR_VIP = 0.95;
static int STATIC_VARIABLE_CURR_PACKAGE_ONLY = 100;
  1. 静态方法
  • 静态方法中不能使用this自引用,其他和成员方法一样。
  • 静态方法不属于某个实例,直接使用类名调用,所以静态方法中不能直接访问成员变量。
  • 在静态方法里边,可以自己创建对象,或者通过参数,获取对象的引用调用方法和成员变量。
  • 在当前类中访问静态方法可以省略类名
  • 使用import static引入一个静态方法或静态变量。
  • 静态方法的重载也是一样的,方法签名不同即可:方法名+参数类型
  • 判断调用哪个方法,也是根据调用时参数匹配决定的。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.geekbang;

import com.geekbang.supermarket.MerchandiseV2;
import static com.geekbang.supermarket.MerchandiseV2.getVIPDiscount;

public class MerchandiseV2DescAppMain {
public static void main(String[] args) {
MerchandiseV2 merchandise = new MerchandiseV2
("书桌", "DESK9527", 40, 999.9, 500);

merchandise.describe();

// >> TODO 使用import static来引入一个静态方法,就可以直接用静态变量名访问了
// TODO import static也可以使用通配符*来引入一个类里所有静态变量
System.out.println(getVIPDiscount());

}
}
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
80
81
82
83
84
85
86
87
88
89
90
91
package com.geekbang.supermarket;

public class MerchandiseV2 {

public String name;
public String id;
public int count;
public double soldPrice;
public double purchasePrice;

// >> TODO 静态变量使用 static 修饰符
public static double DISCOUNT_FOR_VIP = 0.95;

// >> TODO 静态方法使用static修饰符。
// 静态方法的方法名没有约定俗称全大写
public static double getVIPDiscount() {
// >> TODO 静态方法可以访问静态变量,包括自己类的静态变量和在访问控制符允许的别的类的静态变量
return DISCOUNT_FOR_VIP;
}

// >> TODO 除了没有this,静态方法的定义和成员方法一样,也有方法名,返回值和参数
// >> TODO 静态方法没有this自引用,它不属于某个实例,调用的时候也无需引用,直接用类名调用,所以它也不能直接访问成员变量
// >> TODO 当然在静态方法里面,也可以自己创建对象,或者通过参数,获得对象的引用,进而调用方法和访问成员变量
// >> TODO 静态方法只是没有this自引用的方法而已。
public static double getDiscountOnDiscount(LittleSuperMarket littleSuperMarket) {
double activityDiscount = littleSuperMarket.activityDiscount;
return DISCOUNT_FOR_VIP * activityDiscount;
}

public MerchandiseV2(String name, String id, int count, double soldPrice, double purchasePrice) {
this.name = name;
this.id = id;
this.count = count;
this.soldPrice = soldPrice;
this.purchasePrice = purchasePrice;
// soldPrice = 9/0;
}

public String getName() {
return name;
}

public MerchandiseV2(String name, String id, int count, double soldPrice) {
// double purPrice = soldPrice * 0.8;
// this(name, id, count, soldPrice, purchasePrice);
this(name, id, count, soldPrice, soldPrice * 0.8);
// double purPrice = soldPrice * 0.8;
}

public MerchandiseV2() {
this("无名", "000", 0, 1, 1.1);

}

public void describe() {
System.out.println("商品名字叫做" + name + ",id是" + id + "。 商品售价是" + soldPrice
+ "。商品进价是" + purchasePrice + "。商品库存量是" + count +
"。销售一个的毛利润是" + (soldPrice - purchasePrice) + "。折扣为" + DISCOUNT_FOR_VIP);
}

public double calculateProfit() {
double profit = soldPrice - purchasePrice;
// if(profit <= 0){
// return 0;
// }
return profit;
}


public double buy() {
return buy(1);
}

public double buy(int count) {
return buy(count, false);
}

public double buy(int count, boolean isVIP) {
if (this.count < count) {
return -1;
}
this.count -= count;
double totalCost = count * soldPrice;
if (isVIP) {
// >> TODO 静态方法的访问和静态变量一样,可以带上类名,当前类可以省略类名
return totalCost * getVIPDiscount();
} else {
return totalCost;
}
}
}

1.6.3 方法调用内存分析

  • 方法没有被调用的时候,都在方法区中的字节码文件(.class)中存储。
  • 方法被调用的时候,需要进入到栈内存中运行。方法每调用一次就会在栈中有一个入栈操作
  • 当方法执行结束后,会释放该内存,称为出栈

1.6.4 方法的重载

  • 方法签名 : 方法名+依次参数类型。注意,返回值不属于方法签名。方法签名是一个方法在一个类中的唯一标识
  • 同一个类中方法可以重名,但是签名不可以重复。一个类中如果定义了名字相同,签名不同的方法,就叫做方法的重载
  1. 重载的参数匹配规则

如果有如下重载方法,在java中的自动类型转换匹配逻辑为:

1
2
public double buy(int count) {}
public double buy(double count){}

如果传的参数是 byte, short, int,类型会调用buy(int count)方法,如果是long, float, double 类型会调用buy(double count)方法。

  • 无论是否重载参数类型可以不完全匹配的规则是”实参数可以自动类型转换成形参类型”
  • 重载的特殊之处是,参数满足自动类型转换的方法有好几个,重载的规则是选择最”近”的去调用

1.6.5 可变个数的形参

格式:

1
2
//JDK5.0:采用可变个数形参来定义方法,传入多个同一类型变量 
public static void test(int a ,String...books);

特点:

  1. 方法的参数部分有可变形参,需要放在形参声明的最后
  2. 在一个方法的形参中,最多只能声明一个可变个数的形参
  3. 可变参数方法的使用与方法参数部分使用数组是一致的,二者不能同时声明,否则报错

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class StringTools {
String concat(char seperator, String... args){
String str = "";
for (int i = 0; i < args.length; i++) {
if(i==0){
str += args[i];
}else{
str += seperator + args[i];
}
}
return str;
}
}

1.6.6 参数传递机制:值传递

Java里方法的参数传递方式只有一种:值传递。

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

2 包和访问修饰符

2.1 package(包)

  • package语句作为Java源文件的第一条语句出现。若缺省该语句,则指定为无名包。
  • 不同的包里可以有相同名字的类
  • 一个类只能有一个 package 语句,如果有 package 语句,则必须是类的第一行有效代码
  • 包通常使用所在公司域名的倒置:com.atguigu.xxx
1
package 顶层包名.子包名 ;

JDK中主要的包介绍:

  1. java.lang 包含一些Java语言的核心类,如 String、Math、Integer、System和Thread,提供常用功能
  2. java.net 包含执行与网络相关的操作的类和接口
  3. java.io 包含能提供多种输入、输出功能的类
  4. java.util 包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数
  5. java.text 包含一些java格式化相关的类
  6. java.sql 包含了java 进行JDBC数据库编程的相关类、接口
  7. java.awt 包含了构成抽象窗口工具类的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)

2.2 import(导入)

  1. 使用import

每次使用都带包名很繁琐, 可以在使用的类的上面使用 import 语句, 一次性解决问题, 就可以直接使用类了。

1
2
import com.geekbang.supermarket.LittleSuperMarket;
import com.geekbang.supermarket.MerchandiseV2;
  • 如果使用 a.* 导入结构,表示可以导入a包下的所有的结构。举例:可以使用java.util.*的方式,一 次性导入util包下所有的类或接口。
  • 如果导入的类或接口是java.lang包下的,或者是当前包下的,则可以省略此import语句。
  • 如果已经导入java.a包下的类,那么如果需要使用a包的子包下的类的话,仍然需要导入。
  • 如果在代码中使用不同包下的同名的类,那么就需要使用类的全类名的方式指明调用的是哪个类。
  • import static 组合的使用:调用指定类或接口下的静态的属性或方法

2.2 访问修饰符

  1. 属性访问修饰符:public
  • 被 public 修饰的属性,可以被任意包中的类访问
  • 没有访问修饰符的属性,称作缺省的访问修饰符,可以被本包内的其他类和自己的对象
  • 访问修饰符是一种限制或者允许属性访问的修饰符
  1. 属性访问修饰符:protected
  • 被protected修饰,可以在本类使用
  • 可以在本包子类和非子类使用
  • 在其他包中仅限子类可见
  1. 属性访问修饰符:priviate,仅可在本类可见
  2. 属性访问修饰符:缺省,可在本类和本包子类非子类可见,其他包不可见

实现封装就是控制类或成员的可见性范围。这就需要依赖访问控制修饰符,也称为权限修饰符来控制。

修饰符 本类 本包 其他包子类 其他包非子类
private × × ×
缺省 √(本包子类非子类都可见) × ×
protected √(本包子类非子类都可见) √(其他包仅限于子类中可见) ×
public
6. 类的全限定名
  • 包名 + 类名 = 类的全限定名。也可以简称为类的全名
  • 同一个 Java 程序中全限定名字不可重复

3 关键字:this

3.1 实例方法或构造器中使用当前对象的成员

当形参与成员变量同名时,如果在方法内或构造器内需要使用成员变量,必须添加this来表明该变量是类的成员变量。即:我们可以用this来区分成员变量局部变量。比如:

image.png

使用this访问属性和方法时,如果在本类中未找到,会从父类中查找。

3.2 同一个类中构造器互相调用

  • 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
public class Student {
private String name;
private int age;

// 无参构造
public Student() {
// this("",18);//调用本类有参构造器
}

// 有参构造
public Student(String name) {
this();//调用本类无参构造器
this.name = name;
}
// 有参构造
public Student(String name,int age){
this(name);//调用本类中有一个String参数的构造器
this.age = age;
}

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;
}

public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
}

4 继承

4.1 继承的语法

通过 extends 关键字,可以声明一个类B继承另外一个类A,定义格式如下:

1
2
3
4
5
6
7
[修饰符] class 类A {
...
}

[修饰符] class 类B extends 类A {
...
}

4.2 继承中的基本概念

类B,称为子类、派生类(derived class)、SubClass
类A,称为父类、超类、基类(base class)、SuperClass

4.3 继承性的细节

  • 子类继承了父类的方法和属性
  • 使用子类的引用可以调用父类的公有方法
  • 使用子类的引用可以访问父类的公有属性
  • 子类不能直接访问父类中私有的(private)的成员变量和方法,可通过继承的get/set方法进行访问。如图所示:

image.png

  • Java只支持单继承,不支持多重继承

image.png

4.4 方法的重写(override/overwrite)

子类可以对从父类中继承来的方法进行改造,我们称为方法的重写 (override、overwrite)。也称为方法的重置覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.atguigu.inherited.method;

public class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.atguigu.inherited.method;

//SmartPhone:智能手机
public class SmartPhone extends Phone{
//重写父类的来电显示功能的方法
@Override
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
}
//重写父类的通话功能的方法
@Override
public void call() {
System.out.println("语音通话 或 视频通话");
}
}

@Override使用说明:
写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰的知道这是一个重写的方法。

  1. 子类重写的方法必须和父类被重写的方法具有相同的方法名称参数列表
  2. 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型。(例如:Student < Person)。

    注意:如果返回值类型是基本数据类型和void,那么必须是相同

  3. 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限。(public > protected > 缺省 > private)

    注意:① 父类私有方法不能重写 ② 跨包的父类缺省的方法也不能重写

  4. 子类方法抛出的异常不能大于父类被重写方法的异常
    此外,子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。

4.5 关键字:super

在Java类中使用super来调用父类中的指定操作:

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

注意:

  • 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
  • super的追溯不仅限于直接父类
  • super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识

4.5.1 子类中调用父类被重写的方法

  • 如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法;
  • 如果子类重写了父类的方法,在子类中需要通过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
package com.atguigu.inherited.method;

public class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}

//smartphone:智能手机
public class SmartPhone extends Phone{
//重写父类的来电显示功能的方法
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");

//保留父类来电显示号码的功能
super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
}
}

总结:

  • 方法前面没有super.和this.
    • 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
  • 方法前面有this.
    • 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
  • 方法前面有super.
    • 从当前子类的直接父类找,如果没有,继续往上追溯

4.5.2 子类中调用父类中同名的成员变量

  • 如果实例变量与局部变量重名,可以在实例变量前面加this.进行区别
  • 如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量
  • 如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问

4.5.3 子类构造器中调用父类构造器

① 子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方式调用父类指定的构造器。
② 规定:“super(形参列表)”,必须声明在构造器的首行。
③ 我们前面讲过,在构造器的首行可以使用”this(形参列表)”,调用本类中重载的构造器,
结合②,结论:在构造器的首行,”this(形参列表)” 和 “super(形参列表)”只能二选一。
④ 如果在子类构造器的首行既没有显示调用”this(形参列表)”,也没有显式调用”super(形参列表)”,
​ 则子类此构造器默认调用”super()”,即调用父类中空参的构造器。
⑤ 由③和④得到结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。
只能是这两种情况之一。
⑥ 由⑤得到:一个类中声明有n个构造器,最多有n-1个构造器中使用了”this(形参列表)”,则剩下的那个一定使用”super(形参列表)”。

开发中常见错误:
如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参的构造器,则编译出错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
System.out.println("B类无参构造器");
}
}
class Test05{
public static void main(String[] args){
B b = new B();
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个无参构造,
//B类的无参构造没有写super(...),表示默认调用A类的无参构造
//编译报错,因为A类没有无参构造
}
}

image.png

4. 6 继承里的静态方法:

静态方法可以被继承,静态方法不支持多态逻辑,建议在使用静态方法时直接使用类名调用静态方法,如果此类下没有这个方法,则会调用其父类下的静态方法。

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package com.geekbang.supermarket;

public class Phone extends MerchandiseV2 {

// 给Phone增加新的属性和方法
private double screenSize;
private double cpuHZ;
private int memoryG;
private int storageG;
private String brand;
private String os;
private static int MAX_BUY_ONE_ORDER = 5;

// >> TODO 使用super可以调用父类的方法和属性(当然必须满足访问控制符的控制)
public double buy(int count) {
if (count > MAX_BUY_ONE_ORDER) {
System.out.println("购买失败,手机一次最多只能买" + MAX_BUY_ONE_ORDER + "个");
return -2;
}
return super.buy(count);
}

public String getName() {
return this.brand + ":" + this.os + ":" + name;
}

public void describe() {
System.out.println("此手机商品属性如下");
super.describe();
System.out.println("手机厂商为" + brand + ";系统为" + os + ";硬件配置如下:\n" +
"屏幕:" + screenSize + "寸\n" +
"cpu主频" + cpuHZ + " GHz\n" +
"内存" + memoryG + "Gb\n" +
"存储空间" + storageG + "Gb");
}

// >> TODO super是子类和父类交流的桥梁,但是并不是父类的引用
// >> TODO 所以,super和this自引用不一样,不是简单可以模拟的(可以模拟的话不就成了组合了吗)
// public MerchandiseV2 getParent(){
// return super;
// }

public Phone getThisPhone(){
return this;
}

// >> TODO 使用super可以调用父类的public属性,但是super不是一个引用。
public void accessParentProps() {
System.out.println("父类里的name属性:" + super.name);
}

public void useSuper() {
// >> TODO super的用法就像是一个父类的引用。它是继承的一部分,像组合的那部分,但不是全部
super.describe();
super.buy(66);
System.out.println("父类里的count属性:" + super.count);
}

public Phone(
String name, String id, int count, double soldPrice, double purchasePrice,
double screenSize, double cpuHZ, int memoryG, int storageG, String brand, String os
) {
// >> TODO 可以认为,创建子类对象的时候,也就同时创建了一个隐藏的父类对象

this.screenSize = screenSize;
this.cpuHZ = cpuHZ;
this.memoryG = memoryG;
this.storageG = storageG;
this.brand = brand;
this.os = os;

// >> TODO 所以,才能够setName,对name属性进行操作。
this.setName(name);
this.setId(id);
this.setCount(count);
this.setSoldPrice(soldPrice);
this.setPurchasePrice(purchasePrice);
}

public boolean meetCondition() {
return true;
}

public double getScreenSize() {
return screenSize;
}

public void setScreenSize(double screenSize) {
this.screenSize = screenSize;
}

public double getCpuHZ() {
return cpuHZ;
}

public void setCpuHZ(double cpuHZ) {
this.cpuHZ = cpuHZ;
}

public int getMemoryG() {
return memoryG;
}

public void setMemoryG(int memoryG) {
this.memoryG = memoryG;
}

public int getStorageG() {
return storageG;
}

public void setStorageG(int storageG) {
this.storageG = storageG;
}

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public String getOs() {
return os;
}

public void setOs(String os) {
this.os = os;
}
}

4.7 父类和子类的引用赋值关系

  • 父类引用可以指向子类对象,子类引用不可以指向父类的对象
  • 可以进行强制类型转换,如果类型不对,会报错
  • 可以调用的方法,是受引用类型决定的
  • 因为子类继承了父类的方法和属性,所以父类的对象能做到的,子类的对象肯定能做到
  • 如果确定一个父类的引用指向的对象,实际上就是一个子类的对象(或者子类的子类的对象),可以强制类型转换,如果不是,则编译报错

如下示例:
类之间的关系为:LittleSuperMarket -> MerchandiseV2 -> Phone -> ShellColorChangePhone

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
package com.geekbang;

import com.geekbang.supermarket.MerchandiseV2;
import com.geekbang.supermarket.Phone;
import com.geekbang.supermarket.ShellColorChangePhone;

public class ReferenceAssign {
public static void main(String[] args) {
Phone ph = new Phone(
"手机001", "Phone001", 100, 1999, 999,
4.5, 3.5, 4, 128, "索尼", "安卓"
);

// >> TODO 可以用子类的引用给父类的引用赋值,也就是说,父类的引用可以指向子类的对象

MerchandiseV2 m = ph;
MerchandiseV2 m2 = new Phone(
"手机002", "Phone002", 100, 1999, 999,
4.5, 3.5, 4, 128, "索尼", "安卓"
);

// >> TODO 但是反之则不行,不能让子类的引用指向父类的对象。因为父类并没有子类的属性和方法呀

// Phone notDoable = new MerchandiseV2();

// >> TODO 重点
// >> TODO 因为子类继承了父类的方法和属性,所以父类的对象能做到的,子类的对象肯定能做到
// TODO 换句话说,我们可以在子类的对象上,执行父类的方法
// >> TODO 当父类的引用指向子类的实例(或者父类的实例),只能通过父类的引用,像父类一样操作子类的对象
// TODO 也就是说"名"的类型,决定了能执行哪些操作


// >> TODO ph和m都指向同一个对象,通过ph可以调用getBrand方法
// TODO 因为ph的类型是Phone,Phone里定义了getBrand方法
ph.getBrand();
// >> TODO ph和m都指向同一个对象,但是通过m就不可以调用getBrand方法
// TODO 因为m的类型是MerchandiseV2,MerchandiseV2里没有你定义getBrand方法
// m.getBrand();

// TODO 如果确定一个父类的引用指向的对象,实际上就是一个子类的对象(或者子类的子类的对象),可以强制类型转换
Phone aPhone = (Phone) m2;

// MerchandiseV2是Phone的父类,Phone是shellColorChangePhone的父类
ShellColorChangePhone shellColorChangePhone = new ShellColorChangePhone(
"手机002", "Phone002", 100, 1999, 999,
4.5, 3.5, 4, 128, "索尼", "安卓"
);

// TODO 父类的引用,可以指向子类的对象,即可以用子类(以及子类的子类)的引用给父类的引用赋值
MerchandiseV2 ccm = shellColorChangePhone;

// TODO 父类的引用,可以指向子类的对象。
// TODO 确定MerchandiseV2的引用ccm是指向的是Phone或者Phone的子类对象,那么可以强制类型转换
Phone ccp = (Phone) ccm;

// TODO 确定MerchandiseV2的引用ccm是指向的是ShellColorChangePhone或者ShellColorChangePhone的子类对象
// TODO 那么可以强制类型转换
ShellColorChangePhone scp = (ShellColorChangePhone) ccm;

// TODO 会出错,因为m2指向的是一个Phone类型的对象,不是ShellColorChangePhone的对象
ShellColorChangePhone notCCP = (ShellColorChangePhone) m2;


}
}

5 多态性

5.1 多态的形式和体现

对象的多态:在Java中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象

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

  • 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
  • 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法)
    “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)

多态的使用前提:1. 类的继承关系 2. 方法的重写

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.atguigu.polymorphism.grammar;

public class Pet {
private String nickname; //昵称

public String getNickname() {
return nickname;
}

public void setNickname(String nickname) {
this.nickname = nickname;
}

public void eat(){
System.out.println(nickname + "吃东西");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.polymorphism.grammar;

public class Cat extends Pet {
//子类重写父类的方法
@Override
public void eat() {
System.out.println("猫咪" + getNickname() + "吃鱼仔");
}

//子类扩展的方法
public void catchMouse() {
System.out.println("抓老鼠");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.polymorphism.grammar;

public class Dog extends Pet {
//子类重写父类的方法
@Override
public void eat() {
System.out.println("狗子" + getNickname() + "啃骨头");
}

//子类扩展的方法
public void watchHouse() {
System.out.println("看家");
}
}

1、方法内局部变量的赋值体现多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.atguigu.polymorphism.grammar;

public class TestPet {
public static void main(String[] args) {
//多态引用
Pet pet = new Dog();
pet.setNickname("小白");

//多态的表现形式
/*
编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;
*/
pet.eat();//运行时执行子类Dog重写的方法
// pet.watchHouse();//不能调用Dog子类扩展的方法

pet = new Cat();
pet.setNickname("雪球");
pet.eat();//运行时执行子类Cat重写的方法
}
}

2、方法的形参声明体现多态

1
2
3
4
5
6
7
8
9
10
11
package com.atguigu.polymorphism.grammar;

public class Person{
private Pet pet;
public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
this.pet = pet;
}
public void feed(){
pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.atguigu.polymorphism.grammar;

public class TestPerson {
public static void main(String[] args) {
Person person = new Person();

Dog dog = new Dog();
dog.setNickname("小白");
person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型
person.feed();

Cat cat = new Cat();
cat.setNickname("雪球");
person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
person.feed();
}
}

3、方法返回值类型体现多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.polymorphism.grammar;

public class PetShop {
//返回值类型是父类类型,实际返回的是子类对象
public Pet sale(String type){
switch (type){
case "Dog":
return new Dog();
case "Cat":
return new Cat();
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.atguigu.polymorphism.grammar;

public class TestPetShop {
public static void main(String[] args) {
PetShop shop = new PetShop();

Pet dog = shop.sale("Dog");
dog.setNickname("小白");
dog.eat();

Pet cat = shop.sale("Cat");
cat.setNickname("雪球");
cat.eat();
}
}

5.2 多态的好处和弊端

好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。

1
2
3
4
5
6
Student m = new Student();  
m.school = "pku"; //合法,Student类有school成员变量
Person e = new Student();
e.school = "pku"; //非法,Person类没有school成员变量

// 属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。

开发中:使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。

5.3 虚方法调用

在Java中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。

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

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
image.png
前提:Person类中定义了welcome()方法,各个子类重写了welcome()。

拓展:
静态链接(或早起绑定):当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。那么调用这样的方法,就称为非虚方法调用。比如调用静态方法、私有方法、final方法、父类构造器、本类重载构造器等。
动态链接(或晚期绑定):如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。调用这样的方法,就称为虚方法调用。比如调用重写的方法(针对父类)、实现的方法(针对接口)。

5.5 instanceof操作符

instanceof 操作符,可以判断一个引用指向的对象是否是某一个类型或者其子类,是则返回true,否则返回false。

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
package com.geekbang;

import com.geekbang.supermarket.LittleSuperMarket;
import com.geekbang.supermarket.MerchandiseV2;
import com.geekbang.supermarket.Phone;
import com.geekbang.supermarket.ShellColorChangePhone;

public class InstanceOfTestAppMain {
public static void main(String[] args) {
int merchandiseCount = 600;
LittleSuperMarket superMarket = new LittleSuperMarket("大卖场",
"世纪大道1号", 500, merchandiseCount, 100);

// >> TODO instanceof 操作符,可以判断一个引用指向的对象是否是某一个类型或者其子类
// TODO 是则返回true,否则返回false
for(int i =0;i<merchandiseCount;i++){
MerchandiseV2 m = null;// superMarket.getMerchandiseOf(i);
if(m instanceof MerchandiseV2){
// TODO 先判断,再强制类型转换,比较安全
MerchandiseV2 ph = (MerchandiseV2)m;
System.out.println(ph.getName());
}else {
System.out.println("not an instance");
}
}

// >> TODO 如果引用是null,则肯定返回false


}
}

6 Object类的使用

6.1 根父类

所有的类,都直接或间接地继承自Object类。
Object类的对象可以指向任意类,Object类中没有属性,只有方法,定义的对象如下:

6.2 Object类中的方法:

6.2.1 equals

在Object类中定义判断两个对象的逻辑如下:

比较两个引用是否指向的是相同的类实例。

equals方法也是所有自定义类中覆盖实现比较多的方法,通过重新实现equals方法的逻辑,比较自定义类的对象。

  • String类中对Object的使用

程序示例:

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
package com.geekbang;

import com.geekbang.supermarket.LittleSuperMarket;

import java.util.Scanner;

public class StringEqualsAppMain {
public static void main(String[] args) {

LittleSuperMarket superMarket = new LittleSuperMarket("大卖场",
"世纪大道1号", 500, 600, 100);

String s1 = "aaabbb";

String s2 = "aaa" + "bbb";

// >> TODO 说好的每次创建一个新的String对象呢?
System.out.println("s1和s2用==判断结果:"+(s1 == s2));

System.out.println("s1和s2用 equals 判断结果:"+(s1.equals(s2)));

// >> TODO 打乱Java对String的的优化,再试试看
Scanner scanner = new Scanner(System.in);

System.out.println("请输入s1");
s1 = scanner.nextLine();

System.out.println("请输入s2");
s2 = scanner.nextLine();

System.out.println("s1和s2用==判断结果:"+(s1 == s2));

System.out.println("s1和s2用 equals 判断结果:"+(s1.equals(s2)));
}

}

输出:

1
2
3
4
5
6
7
8
s1和s2用==判断结果:true
s1和s2用 equals 判断结果:true
请输入s1
C:\Users\baihaoliang\.jdks\corretto-1.8.0_382\bin\java.exe -javaagent:D:\JetBrains\apps\IDEA-U\ch-0\231.9225.16\lib\idea_rt.jar=12228:D:\JetBrains\apps\IDEA-U\ch-0\231.9225.16\bin -Dfile.encoding=U
请输入s2
C:\Users\baihaoliang\.jdks\corretto-1.8.0_382\bin\java.exe -javaagent:D:\JetBrains\apps\IDEA-U\ch-0\231.9225.16\lib\idea_rt.jar=12228:D:\JetBrains\apps\IDEA-U\ch-0\231.9225.16\bin -Dfile.encoding=U
s1和s2用==判断结果:false
s1和s2用 equals 判断结果:true

String中定义的字符串是不可变的,所以按说在定义s1和s2时应该是不同的两个引用,但是在使用”==”进行比较时发现是相等的。这是因为在Java中定义字符串时,如果底层有一个相同的字符串,则不会重新定义,会把新的引用指向之前的字符串。
但是在定义的字符串很长时,则会突破这个优化,如上,输入了一个很长的字符串,使用"=="比较是不相同的,但是使用equals是相同的,下边看下String类中对equals方法的覆盖实现:

String中重新实现了equals方法按字符进行遍历比较,所以再长的字符串的情况下依然可以比较。

面试题:==和equals的区别

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

6.2.2 hashCode

hashCode可以翻译为哈希码,或者散列码。应该是一个表示对象的特征的int整数。
自定义类中可以进行覆盖实现,来表示依次来表示类的唯一标识。

equals和hashCode是最常覆盖的两个方法,实现逻辑为,equals方法为true,则hashCode就相等,但hashCode相等,equals不一定为true。

6.2.3 toString

方法签名:public String toString()
① 默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式”
② 在进行String与其它类型数据的连接操作时,自动调用toString()方法

1
2
3
Date now=new Date();
System.out.println(“now=”+now); //相当于
System.out.println(“now=”+now.toString());

③ 如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()

因为Java的引用数据类型的变量中存储的实际上时对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM帮你调用了对象的toString()。

④ 可以根据需要在用户自定义类型中重写toString()方法
如String 类重写了toString()方法,返回字符串的值。

1
2
s1="hello";
System.out.println(s1);//相当于System.out.println(s1.toString());

例如自定义的Person类:

1
2
3
4
5
6
7
8
9
public class Person {  
private String name;
private int age;

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

6.2.4 finalize()

  • 当对象被回收时,系统自动调用该对象的 finalize() 方法。(不是垃圾回收器调用的,是本类对象调用的)
    • 永远不要主动调用某个对象的finalize方法,应该交给垃圾回收机制调用。
  • 什么时候被回收:当某个对象没有任何引用时,JVM就认为这个对象是垃圾对象,就会在之后不确定的时间使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize()方法。
  • 子类可以重写该方法,目的是在对象被清理之前执行必要的清理操作。比如,在方法内断开相关连接资源。
    • 如果重写该方法,让一个新的引用变量重新引用该对象,则会重新激活对象。
  • 在JDK 9中此方法已经被标记为过时的。
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 FinalizeTest {
public static void main(String[] args) {
Person p = new Person("Peter", 12);
System.out.println(p);
p = null;//此时对象实体就是垃圾对象,等待被回收。但时间不确定。
System.gc();//强制性释放空间
}
}

class Person{
private String name;
private int age;

public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
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;
}
//子类重写此方法,可在释放对象前进行某些操作
@Override
protected void finalize() throws Throwable {
System.out.println("对象被释放--->" + this);
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}

}

6.2.5 getClass()

public final Class<?> getClass():获取对象的运行时类型

因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法

1
2
3
4
public static void main(String[] args) {
Object obj = new Person();
System.out.println(obj.getClass());//运行时类型
}

结果:

1
class com.atguigu.java.Person

7 关键字:static

7.1 static 关键字

  • 使用范围:
    • 在Java类中,可用static修饰属性、方法、代码块、内部类
  • 被修饰后的成员具备以下特点:
    • 随着类的加载而加载
    • 优先于对象存在
    • 修饰的成员,被所有对象所共享
    • 访问权限允许时,可不创建对象,直接被类调用

7.2 静态变量

  • 静态变量的默认值规则和实例变量一样。
  • 静态变量值是所有对象共享。
  • 静态变量在本类中,可以在任意方法、代码块、构造器中直接使用。
  • 如果权限修饰符允许,在其他类中可以通过“类名.静态变量”直接访问,也可以通过“对象.静态变量”的方式访问(但是更推荐使用类名.静态变量的方式)。
  • 静态变量的get/set方法也静态的,当局部变量与静态变量重名时,使用“类名.静态变量”进行区分。

7.3 静态方法

  • 静态方法在本类的任意方法、代码块、构造器中都可以直接被调用。
  • 只要权限修饰符允许,静态方法在其他类中可以通过“类名.静态方法“的方式调用。也可以通过”对象.静态方法“的方式调用(但是更推荐使用类名.静态方法的方式)。
  • 在static方法内部只能访问类的static修饰的属性或方法,不能访问类的非static的结构。
  • 静态方法可以被子类继承,但不能被子类重写。
  • 静态方法的调用都只看编译时类型。
  • 因为不需要实例就可以访问static方法,因此static方法内部不能有this,也不能有super。如果有重名问题,使用“类名.”进行区别。

示例:

1
2
3
4
5
6
7
8
9
10
11
package com.atguigu.keyword;

public class Father {
public static void method(){
System.out.println("Father.method");
}

public static void fun(){
System.out.println("Father.fun");
}
}
1
2
3
4
5
6
7
8
package com.atguigu.keyword;

public class Son extends Father{
// @Override //尝试重写静态方法,加上@Override编译报错,去掉Override不报错,但是也不是重写
public static void fun(){
System.out.println("Son.fun");
}
}
1
2
3
4
5
6
7
8
9
10
11
package com.atguigu.keyword;

public class TestStaticMethod {
public static void main(String[] args) {
Father.method();
Son.method();//继承静态方法

Father f = new Son();
f.method();//执行Father类中的method
}
}

8 代码块

8.1 静态代码块

  1. 可以有输出语句。
  2. 可以对类的属性、类的声明进行初始化操作。
  3. 不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法
  4. 若有多个静态的代码块,那么按照从上到下的顺序依次执行。
  5. 静态代码块的执行要先于非静态代码块。
  6. 静态代码块随着类的加载而加载,且只执行一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.atguigu.keyword;

public class Chinese {
// private static String country = "中国";

private static String country;
private String name;

{
System.out.println("非静态代码块,country = " + country);
}

static {
country = "中国";
System.out.println("静态代码块");
}

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

8.2 非静态代码块

如果多个重载的构造器有公共代码,并且这些代码都是先于构造器其他代码执行的,那么可以将这部分代码抽取到非静态代码块中,减少冗余代码。

  1. 可以有输出语句。
  2. 可以对类的属性、类的声明进行初始化操作。
  3. 除了调用非静态的结构外,还可以调用静态的变量或方法。
  4. 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
  5. 每次创建对象的时候,都会执行一次。且先于构造器执行。

9 final 修饰符

9.1 final修饰类:不可被继承

1
2
3
4
5
6
final class Eunuch{//太监类

}
class Son extends Eunuch{//错误

}

9.2 final修饰方法:不可被子类覆盖

1
2
3
4
5
6
7
8
9
10
class Father{
public final void method(){
System.out.println("father");
}
}
class Son extends Father{
public void method(){//错误
System.out.println("son");
}
}

9.3 final 修饰变量

final修饰某个变量(成员变量或局部变量),一旦赋值,它的值就不能被修改,即常量,常量名建议使用大写字母。
例如:final double MY_PI = 3.14;

  1. 构造方法不能用final修饰

  2. 使用final修饰方法的形参后,则无法在方法内修改形参变量

  3. 修饰静态变量:需要初始化,或者在static块中初始化,并且初始化后无法修改。

1
2
3
4
5
6
private final static int MAX_BUY_ONE_ORDER = 9;
//或 选其一
static {
MAX_BUY_ONE_ORDER = 1;
}

  1. 修饰属性:只能在定义时初始化或者构造方法中初始化,初始化后无法修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public final class Test {
    public static int totalNumber = 5;
    public final int ID;

    public Test() {
    ID = ++totalNumber; // 可在构造器中给final修饰的“变量”赋值
    }
    public static void main(String[] args) {
    Test t = new Test();
    System.out.println(t.ID);
    }
    }
  2. 修饰引用:引用类型本身初始化后无法修改,但可以通过引用修改引用的对象内容。

    1
    2
    3
    4
    5
    6
    int[] array1 = new int[10];
    final int[] array = new int[10];
    array = array1; //报错,无法修改final变量
    for(int b : array) { //for循环的一种新形式
    System.out.println(b);
    }
  3. 修饰局部变量:只能初始化一次,后边不能再次修改

    1
    2
    3
    4
    5
    6
    7
    8
    public class TestFinal {
    public static void main(String[] args){
    final int MIN_SCORE ;
    MIN_SCORE = 0;
    final int MAX_SCORE = 100;
    MAX_SCORE = 200; //非法
    }
    }

10 抽象类与抽象方法

10.1 语法格式

  • 抽象类:被abstract修饰的类。
  • 抽象方法:被abstract修饰没有方法体的方法。

抽象类的语法格式

1
2
3
4
5
6
[权限修饰符] abstract class 类名{

}
[权限修饰符] abstract class 类名 extends 父类{

}

抽象方法的语法格式

1
[其他修饰符] abstract 返回值类型 方法名([形参列表]);

注意:抽象方法没有方法体

代码举例:

1
2
3
public abstract class Animal {
public abstract void eat();
}
1
2
3
4
5
public class Cat extends Animal {
public void eat (){
System.out.println("小猫吃鱼和猫粮");
}
}
1
2
3
4
5
6
7
8
9
public class CatTest {
public static void main(String[] args) {
// 创建子类对象
Cat c = new Cat();

// 调用eat方法
c.eat();
}
}

此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法

10.2 使用说明

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

    理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
    抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。

  2. 抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。

    理解:子类的构造方法中,有默认的super()或手动的super(实参列表),需要访问父类构造方法。

  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。

    理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

10.3 注意事项

  • 不能用abstract修饰变量、代码块、构造器;
  • 不能用abstract修饰私有方法、静态方法、final的方法、final的类。

11 接口(interface)

11.1 接口的声明格式

接口的定义,它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。

引用数据类型:数组,类,枚举,接口,注解。

1
2
3
4
5
6
7
8
9
[修饰符] interface 接口名{
//接口的成员列表:
// 公共的静态常量
// 公共的抽象方法

// 公共的默认方法(JDK1.8以上)
// 公共的静态方法(JDK1.8以上)
// 私有方法(JDK1.9以上)
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.atguigu.interfacetype;

public interface USB3{
//静态常量
long MAX_SPEED = 500*1024*1024;//500MB/s

//抽象方法
void in();
void out();

//默认方法
default void start(){
System.out.println("开始");
}
default void stop(){
System.out.println("结束");
}

//静态方法
static void show(){
System.out.println("USB 3.0可以同步全速地进行读写操作");
}
}

11.2 接口的使用规则

11.2.1 类实现接口

接口不能创建对象,但是可以被类实现(implements ,类似于被继承)。

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。

1
2
3
4
5
6
7
8
9
【修饰符】 class 实现类  implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

image.png

  1. 如果接口的实现类是非抽象类,那么必须重写接口中所有抽象方法
  2. 默认方法可以选择保留,也可以重写。重写时,default单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了
  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
interface USB{		// 
public void start() ;
public void stop() ;
}
class Computer{
public static void show(USB usb){
usb.start() ;
System.out.println("=========== USB 设备工作 ========") ;
usb.stop() ;
}
};
class Flash implements USB{
public void start(){ // 重写方法
System.out.println("U盘开始工作。") ;
}
public void stop(){ // 重写方法
System.out.println("U盘停止工作。") ;
}
};
class Print implements USB{
public void start(){ // 重写方法
System.out.println("打印机开始工作。") ;
}
public void stop(){ // 重写方法
System.out.println("打印机停止工作。") ;
}
};
public class InterfaceDemo{
public static void main(String args[]){
Computer.show(new Flash()) ;
Computer.show(new Print()) ;

c.show(new USB(){
public void start(){
System.out.println("移动硬盘开始运行");
}
public void stop(){
System.out.println("移动硬盘停止运行");
}
});
}
};

11.2.2 接口的多实现

在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。
实现格式:

1
2
3
4
5
6
7
8
9
【修饰符】 class 实现类  implements 接口1,接口2,接口3。。。{
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口1,接口2,接口3。。。{
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次

11.2.3 接口的继承

一个接口能继承另一个或者多个接口,接口的继承也使用 extends 关键字,子接口继承父接口的方法。

Intf1接口:

1
2
3
4
5
package com.geekbang.intf;

public interface Intf1 {
void m1();
}

Intf2接口:

1
2
3
4
5
6
7
8
package com.geekbang.intf;

public interface Intf2 {
void m1();

void m2();

}

Intf3接口:

1
2
3
4
5
6
7
8
package com.geekbang.intf;

// >> TODO 接口也可以继承接口。接口可以继承多个接口,接口之间的继承要用extends
// >> TODO 接口不可以继承类
// >> TODO 继承的接口,可以有重复的方法,但是签名相同时,返回值必须完全一样,否则会有编译错误
public interface Intf3 extends Intf1, Intf2{
void m3();
}

注意:

  • 接口不可以继承类
  • 接口不可以声明实例变量
  • 如果一个类继承了两个接口,并且两个接口中的有相同的缺省方法,则编译报错。
  • 接口中可以有静态方法,不需要用default修饰。静态方法可以被实现接口的类继承
  • 接口中的this自引用,this引用的是实现这个接口的类所定义的实例

11.2.4 有方法的代码的接口

  • 在Java 8中,接口允许有缺省实现的抽象方法。
  • 缺省的实现方法,用default修饰,可以有方法体
  • 接口中可以有私有方法,不需要用default修饰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.geekbang.supermarket;

import java.util.Date;

public interface ExpireDateMerchandise {

// >> TODO 缺省的实现方法,用default修饰,可以有方法体
default boolean notExpireInDays(int days) {
return daysBeforeExpire() > days;
}

// >> TODO 接口中可以有私有方法,不需要用default修饰
// >> TODO 接口里的私有方法,可以认为是代码直接插入到使用的地方
private long daysBeforeExpire() {
long expireMS = getExpireDate().getTime();
long left = expireMS - System.currentTimeMillis();
if (left < 0) {
return -1;
}
// 返回值是long,是根据left的类型决定的
return left / (24 * 3600 * 1000);
}
}

面对有缺省方法的接口,一个类继承时可以有三种选择:

  1. 默默继承,相当于类具有了这个方法的实现
  2. 覆盖,重新实现
  3. 把此方法声明为abstract,相当于把这个方法的实现拒之门外,但有abstrace方法的类,此类也就成了抽象类。

11.2.5 接口与实现类对象构成多态引用

实现类实现接口,类似于子类继承父类,因此,接口类型的变量与实现类的对象之间,也可以构成多态引用。通过接口类型的变量调用方法,最终执行的是你new的实现类对象实现的方法体。

接口的不同实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.atguigu.interfacetype;

public class Mouse implements USB3 {
@Override
public void out() {
System.out.println("发送脉冲信号");
}

@Override
public void in() {
System.out.println("不接收信号");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.interfacetype;

public class KeyBoard implements USB3{
@Override
public void in() {
System.out.println("不接收信号");
}

@Override
public void out() {
System.out.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
25
26
27
28
29
package com.atguigu.interfacetype;

public class TestComputer {
public static void main(String[] args) {
Computer computer = new Computer();
USB3 usb = new Mouse();
computer.setUsb(usb);
usb.start();
usb.out();
usb.in();
usb.stop();
System.out.println("--------------------------");

usb = new KeyBoard();
computer.setUsb(usb);
usb.start();
usb.out();
usb.in();
usb.stop();
System.out.println("--------------------------");

usb = new MobileHDD();
computer.setUsb(usb);
usb.start();
usb.out();
usb.in();
usb.stop();
}
}

11.2.6 使用接口的静态成员

接口不能直接创建对象,但是可以通过接口名直接调用接口的静态方法和静态常量。

1
2
3
4
5
6
7
8
9
10
package com.atguigu.interfacetype;

public class TestUSB3 {
public static void main(String[] args) {
//通过“接口名.”调用接口的静态方法 (JDK8.0才能开始使用)
USB3.show();
//通过“接口名.”直接使用接口的静态常量
System.out.println(USB3.MAX_SPEED);
}
}

11.2.7 使用接口的非静态方法

  • 对于接口的静态方法,直接使用“接口名.”进行调用即可
    • 也只能使用“接口名.”进行调用,不能通过实现类的对象进行调用
  • 对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用
    • 接口不能直接创建对象,只能创建实现类的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.atguigu.interfacetype;

public class TestMobileHDD {
public static void main(String[] args) {
//创建实现类对象
MobileHDD b = new MobileHDD();

//通过实现类对象调用重写的抽象方法,以及接口的默认方法,如果实现类重写了就执行重写的默认方法,如果没有重写,就执行接口中的默认方法
b.start();
b.in();
b.stop();

//通过接口名调用接口的静态方法
// MobileHDD.show();
// b.show();
Usb3.show();
}
}

11.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
public class USBTest {  

public static void main(String[] args) {

//1.创建接口实现类的对象
Computer computer = new Computer();
Printer printer = new Printer();

computer.transferData(printer);

//2.创建接口实现类的匿名对象
computer.transferData(new Camera());
System.out.println();

//3.创建接口匿名实现类的对象
USB usb1 = new USB(){
public void start(){
System.out.println("U盘开始工作");
}
public void stop(){
System.out.println("U盘结束工作");
}
};
computer.transferData(usb1);

//4. 创建接口匿名实现类的匿名对象

computer.transferData(new USB(){
public void start(){
System.out.println("扫描仪开始工作");
}
public void stop(){
System.out.println("扫描仪结束工作");
}
});

}
}

11.4 接口与抽象类对比

image.png

12 内部类(InnerClass)

将一个类A定义在另一个类B里面,里面的那个类A就称为内部类(InnerClass),类B则称为外部类(OuterClass)

根据内部类声明额未知,可以分为:
image.png

12.1 成员内部类

12.1.1 概述

如果成员内部类中不使用外部类的非静态成员,那么通常将内部类声明为静态内部类,否则声明为非静态内部类。
成员内部类的使用特征:

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

注意:

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

12.1.2 创建成员内部类对象

  • 实例化静态内部类

    1
    2
    外部类名.静态内部类名 变量 = 外部类名.静态内部类名();
    变量.非静态方法();
  • 实例化非静态内部类

    1
    2
    3
    外部类名 变量1 = new 外部类();
    外部类名.非静态内部类名 变量2 = 变量1.new 非静态内部类名();
    变量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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class TestMemberInnerClass {
public static void main(String[] args) {
//创建静态内部类实例,并调用方法
Outer.StaticInner inner = new Outer.StaticInner();
inner.inFun();
//调用静态内部类静态方法
Outer.StaticInner.inMethod();

System.out.println("*****************************");

//创建非静态内部类实例(方式1),并调用方法
Outer outer = new Outer();
Outer.NoStaticInner inner1 = outer.new NoStaticInner();
inner1.inFun();

//创建非静态内部类实例(方式2)
Outer.NoStaticInner inner2 = outer.getNoStaticInner();
inner1.inFun();
}
}
class Outer{
private static String a = "外部类的静态a";
private static String b = "外部类的静态b";
private String c = "外部类对象的非静态c";
private String d = "外部类对象的非静态d";

static class StaticInner{
private static String a ="静态内部类的静态a";
private String c = "静态内部类对象的非静态c";
public static void inMethod(){
System.out.println("Inner.a = " + a);
System.out.println("Outer.a = " + Outer.a);
System.out.println("b = " + b);
}
public void inFun(){
System.out.println("Inner.inFun");
System.out.println("Outer.a = " + Outer.a);
System.out.println("Inner.a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
// System.out.println("d = " + d);//不能访问外部类的非静态成员
}
}

class NoStaticInner{
private String a = "非静态内部类对象的非静态a";
private String c = "非静态内部类对象的非静态c";

public void inFun(){
System.out.println("NoStaticInner.inFun");
System.out.println("Outer.a = " + Outer.a);
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("Outer.c = " + Outer.this.c);
System.out.println("c = " + c);
System.out.println("d = " + d);
}
}


public NoStaticInner getNoStaticInner(){
return new NoStaticInner();
}
}

12.2 局部内部类

12.2.1 非匿名局部内部类

语法格式:

1
2
3
4
5
6
[修饰符] class 外部类{
[修饰符] 返回值类型 方法名(形参列表){
[final/abstract] class 内部类{
}
}
}
  • 编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名、$符号、编号。
    • 这里有编号是因为同一个外部类中,不同的方法中存在相同名称的局部内部类
  • 和成员内部类不同的是,它前面不能有权限修饰符等
  • 局部内部类如同局部变量一样,有作用域
  • 局部内部类中是否能访问外部类的非静态的成员,取决于所在的方法

示例:

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
/**
* ClassName: TestLocalInner
* @Author 尚硅谷-宋红康
* @Create 17:19
* @Version 1.0
*/
public class TestLocalInner {
public static void main(String[] args) {
Outer.outMethod();
System.out.println("-------------------");

Outer out = new Outer();
out.outTest();
System.out.println("-------------------");

Runner runner = Outer.getRunner();
runner.run();

}
}
class Outer{

public static void outMethod(){
System.out.println("Outer.outMethod");
final String c = "局部变量c";
class Inner{
public void inMethod(){
System.out.println("Inner.inMethod");
System.out.println(c);
}
}

Inner in = new Inner();
in.inMethod();
}

public void outTest(){
class Inner{
public void inMethod1(){
System.out.println("Inner.inMethod1");
}
}

Inner in = new Inner();
in.inMethod1();
}

public static Runner getRunner(){
class LocalRunner implements Runner{
@Override
public void run() {
System.out.println("LocalRunner.run");
}
}
return new LocalRunner();
}

}
interface Runner{
void run();
}

12.2.2 匿名内部类

因为考虑到这个子类或实现类是一次性的,那么我们“费尽心机”的给它取名字,就显得多余。那么我们完全可以使用匿名内部类的方式来实现,避免给类命名的问题。

1
2
3
new 父类([实参列表]){
重写方法...
}
1
2
3
new 父接口(){
重写方法...
}

举例1:使用匿名内部类的对象直接调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
interface A{
void a();
}
public class Test{
public static void main(String[] args){
new A(){
@Override
public void a() {
System.out.println("aaaa");
}
}.a();
}
}

举例2:通过父类或父接口的变量多态引用匿名内部类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface A{
void a();
}
public class Test{
public static void main(String[] args){
A obj = new A(){
@Override
public void a() {
System.out.println("aaaa");
}
};
obj.a();
}
}

举例3:匿名内部类的对象作为实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface A{
void method();
}
public class Test{
public static void test(A a){
a.method();
}

public static void main(String[] args){
test(new A(){

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

13 枚举类(enum)

13.1 枚举的定义

1
2
3
4
5
6
7
8
9
【修饰符】 enum 枚举类名{
常量对象列表
}

【修饰符】 enum 枚举类名{
常量对象列表;

对象的实例变量列表;
}

13.2 enum方式定义的要求和特点

  • 枚举类的常量对象列表必须在枚举类的首行,因为是常量,所以建议大写。
  • 列出的实例系统会自动添加 public static final 修饰。
  • 如果常量对象列表后面没有其他代码,那么“;”可以省略,否则不可以省略“;”。
  • 编译器给枚举类默认提供的是private的无参构造,如果枚举类需要的是无参构造,就不需要声明,写常量对象列表时也不用加参数
  • 如果枚举类需要的是有参构造,需要手动定义,有参构造的private可以省略,调用有参构造的方法就是在常量对象名后面加(实参列表)就可以。
  • 枚举类默认继承的是java.lang.Enum类,因此不能再继承其他的类型。
  • switch提供支持枚举类型,case后面可以写枚举常量名,无需添加枚举类作为限定

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum SeasonEnum {
SPRING("春天","春风又绿江南岸"),
SUMMER("夏天","映日荷花别样红"),
AUTUMN("秋天","秋水共长天一色"),
WINTER("冬天","窗含西岭千秋雪");

private final String seasonName;
private final String seasonDesc;

private SeasonEnum(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
}

示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.atguigu.enumeration;

public enum Week {
MONDAY("星期一"),
TUESDAY("星期二"),
WEDNESDAY("星期三"),
THURSDAY("星期四"),
FRIDAY("星期五"),
SATURDAY("星期六"),
SUNDAY("星期日");

private final String description;

private Week(String description){
this.description = description;
}

@Override
public String toString() {
return super.toString() +":"+ description;
}
}

13.3 enum中常用方法

1
2
3
4
5
String toString(): 默认返回的是常量名(对象名),可以继续手动重写该方法!
static 枚举类型[] values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值,是一个静态方法
static 枚举类型 valueOf(String name):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。
String name():得到当前枚举常量的名称。建议优先使用toString()。
int ordinal():返回当前枚举常量的次序号,默认从0开始

13.4 实现接口的枚举类

  • 和普通 Java 类一样,枚举类可以实现一个或多个接口
  • 若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可。
  • 若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1、枚举类可以像普通的类一样,实现接口,并且可以多个,但要求必须实现里面所有的抽象方法!
enum A implements 接口1,接口2{
//抽象方法的实现
}

//2、如果枚举类的常量可以继续重写抽象方法!
enum A implements 接口1,接口2{
常量名1(参数){
//抽象方法的实现或重写
},
常量名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
interface Info{
void show();
}

//使用enum关键字定义枚举类
enum Season1 implements Info{
//1. 创建枚举类中的对象,声明在enum枚举类的首位
SPRING("春天","春暖花开"){
public void show(){
System.out.println("春天在哪里?");
}
},
SUMMER("夏天","夏日炎炎"){
public void show(){
System.out.println("宁静的夏天");
}
},
AUTUMN("秋天","秋高气爽"){
public void show(){
System.out.println("秋天是用来分手的季节");
}
},
WINTER("冬天","白雪皑皑"){
public void show(){
System.out.println("2002年的第一场雪");
}
};

//2. 声明每个对象拥有的属性:private final修饰
private final String SEASON_NAME;
private final String SEASON_DESC;

//3. 私有化类的构造器
private Season1(String seasonName,String seasonDesc){
this.SEASON_NAME = seasonName;
this.SEASON_DESC = seasonDesc;
}

public String getSEASON_NAME() {
return SEASON_NAME;
}

public String getSEASON_DESC() {
return SEASON_DESC;
}
}

14 包装类

14.1 有那些包装类

Java针对八种基本数据类型定义了相应的引用类型:包装类(封装类)。有了类的特点,就可以调用类中的方法,Java才是真正的面向对象。
image.png

封装以后的,内存结构对比:

1
2
3
4
public static void main(String[] args){
int num = 520;
Integer obj = new Integer(520);
}

image.png

14.2 包装类与基本数据类型间的转换

14.2.1 自动装箱与拆箱

由于我们经常要做基本类型与包装类之间的转换,从JDK5.0 开始,基本类型与包装类的装箱、拆箱动作可以自动完成。例如:

1
2
3
Integer i = 4;//自动装箱。相当于Integer i = Integer.valueOf(4);
i = i + 5;//等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5;
//加法运算完成后,再次装箱,把基本数值转成对象。

注意:只能与自己对应的类型之间才能实现自动装箱与拆箱。

1
2
Integer i = 1;
Double d = 1;//错误的,1是int类型

14.2.2 基本数据类型、包装类与字符串间的转换

(1)基本数据类型转为字符串

方式1: 调用字符串重载的valueOf()方法

1
2
3
4
int a = 10;
//String str = a;//错误的

String str = String.valueOf(a);

方式2: 更直接的方式

1
2
3
int a = 10;

String str = a + "";

(2)字符串转为基本数据类型

方式1: 除了Character类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型,例如:

  • public static int parseInt(String s):将字符串参数转换为对应的int基本类型。
  • public static long parseLong(String s):将字符串参数转换为对应的long基本类型。
  • public static double parseDouble(String s):将字符串参数转换为对应的double基本类型。

方式2: 字符串转为包装类,然后可以自动拆箱为基本数据类型

  • public static Integer valueOf(String s):将字符串参数转换为对应的Integer包装类,然后可以自动拆箱为int基本类型
  • public static Long valueOf(String s):将字符串参数转换为对应的Long包装类,然后可以自动拆箱为long基本类型
  • public static Double valueOf(String s):将字符串参数转换为对应的Double包装类,然后可以自动拆箱为double基本类型

注意:如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出java.lang.NumberFormatException异常。

方式3: 通过包装类的构造器实现

1
2
3
4
5
6
7
8
9
int a = Integer.parseInt("整数的字符串");
double d = Double.parseDouble("小数的字符串");
boolean b = Boolean.parseBoolean("true或false");

int a = Integer.valueOf("整数的字符串");
double d = Double.valueOf("小数的字符串");
boolean b = Boolean.valueOf("true或false");

int i = new Integer(“12”);

其他方式小结:
image.png

14.3 包装类的API

14.3.1 数据类型的最大最小值

1
2
3
Integer.MAX_VALUE和Integer.MIN_VALUE
Long.MAX_VALUE和Long.MIN_VALUE
Double.MAX_VALUE和Double.MIN_VALUE

14.3.2 字符转大小写

1
2
Character.toUpperCase('x');
Character.toLowerCase('X');

14.3.3 整数转进制

1
2
3
Integer.toBinaryString(int i)  
Integer.toHexString(int i)
Integer.toOctalString(int i)

14.3.4 比较的方法

1
2
Double.compare(double d1, double d2)
Integer.compare(int x, int y)

14.4 包装类对象的特点

14.4.1 包装类缓存对象

包装类 缓存对象
Byte -128~127
Short -128~127
Integer -128~127
Long -128~127
Float 没有
Double 没有
Character 0~127
Boolean true和false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Integer a = 1;
Integer b = 1;
System.out.println(a == b); //true

Integer i = 128;
Integer j = 128;
System.out.println(i == j); //false

Integer m = new Integer(1); //新new的在堆中
Integer n = 1; //这个用的是缓冲的常量对象,在方法区
System.out.println(m == n); //false

Integer x = new Integer(1); //新new的在堆中
Integer y = new Integer(1); //另一个新new的在堆中
System.out.println(x == y); //false
1
2
3
Double d1 = 1.0;
Double d2 = 1.0;
System.out.println(d1==d2);//false 比较地址,没有缓存对象,每一个都是新new的

14.4.2 类型转换问题

1
2
3
Integer i = 1000;
double j = 1000;
System.out.println(i==j);//true 会先将i自动拆箱为int,然后根据基本数据类型“自动类型转换”规则,转为double比较
1
2
3
Integer i = 1000;
int j = 1000;
System.out.println(i==j);//true 会自动拆箱,按照基本数据类型进行比较
1
2
3
Integer i = 1;
Double d = 1.0
System.out.println(i==d);//编译报错

14.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
public class TestExam {
public static void main(String[] args) {
int i = 1;
Integer j = new Integer(2);
Circle c = new Circle();
change(i, j, c);
System.out.println("i = " + i); //1
System.out.println("j = " + j); //2
System.out.println("c.radius = " + c.radius); //10.0
}

/*
* 方法的参数传递机制:
* (1)基本数据类型:形参的修改完全不影响实参
* (2)引用数据类型:通过形参修改对象的属性值,会影响实参的属性值
* 这类Integer等包装类对象是“不可变”对象,即一旦修改,就是新对象,和实参就无关了
*/
public static void change(int a, Integer b, Circle c) {
a += 10;
// b += 10;//等价于 b = new Integer(b+10);
c.radius += 10;
/*c = new Circle();
c.radius+=10;*/
}
}
class Circle{
double radius;
}

15 单例(Singleton)设计模式

15.1 什么是单例模式

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

实现思路:
首先必须将类的构造器的访问权限设置为private,这样,就不能用new操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的

15.2 单例模式的两种实现方式

15.2.1 饿汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton {
// 1.私有化构造器
private Singleton() {
}

// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single = new Singleton();

// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
return single;
}
}

15.2.2 懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
// 1.私有化构造器
private Singleton() {
}
// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single;
// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
if(single == null) {
single = new Singleton();
}
return single;
}
}

存在多线程安全问题

使用同步机制:

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
package com.atguigu.single.lazy;

public class LazyOne {
private static LazyOne instance;

private LazyOne(){}

//方式1:
public static synchronized LazyOne getInstance1(){
if(instance == null){
instance = new LazyOne();
}
return instance;
}
//方式2:
public static LazyOne getInstance2(){
synchronized(LazyOne.class) {
if (instance == null) {
instance = new LazyOne();
}
return instance;
}
}
//方式3:
public static LazyOne getInstance3(){
if(instance == null){
synchronized (LazyOne.class) {
try {
Thread.sleep(10);//加这个代码,暴露问题
} catch (InterruptedException e) {
e.printStackTrace();
}
if(instance == null){
instance = new LazyOne();
}
}
}

return instance;
}
/*
注意:上述方式3中,有指令重排问题
mem = allocate(); 为单例对象分配内存空间
instance = mem; instance引用现在非空,但还未初始化
ctorSingleton(instance); 为单例对象通过instance调用构造器
从JDK2开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,然后复制到主存储区。但是需要
volatile关键字,避免指令重排。
*/

}
  • 使用内部类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package com.atguigu.single.lazy;

    public class LazySingle {
    private LazySingle(){}

    public static LazySingle getInstance(){
    return Inner.INSTANCE;
    }

    private static class Inner{
    static final LazySingle INSTANCE = new LazySingle();
    }

    }

内部类只有在外部类被调用才加载,产生INSTANCE实例;又不用加锁。
此模式具有之前两个模式的优点,同时屏蔽了它们的缺点,是最好的单例模式。
此时的内部类,使用enum进行定义,也是可以的。

15.2.3 饿汉式 vs 懒汉式

饿汉式:

  • 特点:立即加载,即在使用类的时候已经将对象创建完毕。
  • 优点:实现起来简单;没有多线程安全问题。
  • 缺点:当类被加载的时候,会初始化static的实例,静态变量被创建并分配内存空间,从这以后,这个static的实例便一直占着这块内存,直到类被卸载时,静态变量被摧毁,并释放所占有的内存。因此在某些特定条件下会耗费内存

懒汉式:

  • 特点:延迟加载,即在调用静态方法时实例才被创建。
  • 优点:实现起来比较简单;当类被加载的时候,static的实例未被创建并分配内存空间,当静态方法第一次被调用时,初始化实例变量,并分配内存,因此在某些特定条件下会节约内存
  • 缺点:在多线程环境中,这种实现方法是完全错误的,线程不安全,根本不能保证单例的唯一性。
    • 说明:在多线程章节,会将懒汉式改造成线程安全的模式。