阶段1 Java入门
- 第1章 Java 程序设计概述
- 第 2 章 Java 程序设计环境
- 第 3 章 Java 基本程序设计结构
- 第4章 对象与类
- 第 5 章 类的封装、继承、多态
- 第6章 接口、lambda 表达式
- ————————————————————————
- 第 7 章 异常、 断言和日志
- 第 8 章 泛型程序设计(Generic programming)
- ————————————————————————-
- 第10 章 多线程:如何创建、进程安全(进程同步)
- ————————————————————————
- 第11 章 常用类和基础API
- ————————————————————————
- 第 12 章 集合:(List、Set)、Map
- ————————————————————————
- 第15 章 IO流:流对象,
- 第16章 网络编程
- 第17章 反射:`动态的`获取指定对象所属的类,创建运行时类的对象,调用指定的结构(属性、方法)等。
- 第18章 新特性
第1章 Java 程序设计概述
1.1 Java特点
▶ \blacktriangleright ▶ Java重要特点
- Java语言是面向对象的(oop)
- Java语言是健壮的。Java的强类型机制、异常处理、垃圾的自动收集等是Java程序健壮
性的重要保证- Java语言是跨平台性的。
- Java语言是解释型的
解释性语言:javascript,PHP,java
编译性语言:c/c++
区别是:解释性语言,编译后的代码,不能直接被机器执行,需要解释器来执行,编译性语言,编译后的代码,可以直接被机器执行,c/c++
第 2 章 Java 程序设计环境
2.1 环境搭建
2.1.1 安装JDK
▶ \blacktriangleright ▶什么是JDK、JRE
- JDK的全称(Java Development Kit Java开发工具包
J D K = J R E + j a v a 的开发工具 JDK=JRE+java的开发工具 JDK=JRE+java的开发工具[java,javac,javadoc,javap等]- JDK是提供给Java开发人员使用的,其中包含了java的开发工具,也包括了JRE,所以安装了JDK,就不用在单独安装JRE了。
▶ \blacktriangleright ▶JRE基本介绍
- JRE(Java Runtime Environment Java运行环境)
JRE=JVM+Java的核心类库[类]- 包括Java虚拟机(JVM Java Virtual Machine和Java程序所需的核心类库等,
如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
▶ \blacktriangleright ▶JDK、JRE和JVM的包含关系
- JDK=JRE+开发工具集(例如Javac,java编译工具等)
- JRE=JVM+Java SE标准类库
- JDK=JVM+Java SE标准类库+开发工具集
- 如果只想运行开发好的.class文件只需要JRE
▶ \blacktriangleright ▶ jdk配置
2.1.2 Seting
▶ \blacktriangleright ▶ seting
- 默认打开上次关闭的工程
- 背景字体分隔符,提示导包设编码,文档注释头
2.2 IDEA 开发工具
1. 新建项目
▶ \blacktriangleright ▶ IDEA使用步骤
- 创建一个空项目(JavaSE Code)
- 创建一个新模块(idea test)
- 在idea test模块下的src下创建一个包(com.itheima)
- 在com.itheima包下新建一个类(HelloWorld)
- 在HelloWorld类中编写代码
▶ \blacktriangleright ▶ 项目结构
project(工程) – module(模块) – package(包) – class(类)
- 项目和模块
- 模块和包
在一个module下,可以声明多个包(package):实现不同功能
▶ \blacktriangleright ▶ IDEA中模块操作分类
- 新建模块
- 删除模块
- 导入模块
2. 辅助键和快捷键
▶ \blacktriangleright ▶ 辅助键和快捷键
- 快速生成语句
快速生成maino方法:psvm,回车
快速生成输出语句:sout,回车- 注释
单行:选中代码,Ctrl+/,再来一次,就是取消
多行:选中代码,Ctrl+Shift+/,再来一次,就是取消
- 自动生成引用变量声明 ★ \bigstar ★
Ctrl+Alt+V- 根据光标所在问题,提供快速修复选择 ★ \bigstar ★
Alt+Enter- 抛出异常 ★ \bigstar ★
Ctrl+Alt+T- 生成Get、Set
alt+enter、alt+insert
▶ \blacktriangleright ▶ Debug
- 添加删除断点
- Debger窗口
2.2.2 包
▶ \blacktriangleright ▶ 类的导入
一个类可以使用所属包中的所有类, 以及其他包中的公有类( public class)。
可以采用两种方式访问另一个包中的公有类。
- 第一种方式:在每个类名之前添加完整的包名。
例:java.time.LocalDate today = java.time.Local Date.now();- 第二种方式:import 语句
∙ \bullet ∙ 使用 import 语句导人一个特定的类或者整个包。
∙ \bullet ∙ 例:导包:import java.util .*;
然后, 就可以使用LocalDate today = Local Date.now();
∙ \bullet ∙ 例:导入特定类:import java.time.LocalDate;- 命名冲突
∙ \bullet ∙ 例:java.util 和 java.sql 包都有日期( Date) 类。
如果在程序中导入了这两个包,在程序使用 Date 类的时候, 就会出现一个编译错误。
∙ \bullet ∙ 解决方法:采用增加一个特定的 import 语句导入确定使用哪一个包中的类。
∙ \bullet ∙ 如果这两个 Date 类都需要使用:在每个类名的前面加上完整的包名。- ★ \bigstar ★Java编译器可以查看其他文件的内部,只要告诉它到哪里去查看就可以了。通过显式地给出包名, 如java.util.Date, 就可以不使用 import ;
▶ \blacktriangleright ▶ 静态导入
import 语句不仅可以导入类,还增加了导人静态方法和静态域的功能。
- 导入该类的所有静态方法和静态域。
例:import static java.lang.System.*;
就可以使用 System 类的静态方法和静态域,而不必加类名前缀。- 导入特定的静态方法或静态域:
例:import static java.lang.System.out;
▶ \blacktriangleright ▶ 将类放入包中
将包的名字放在源文件的开头,包中定义类的代码之前。
- 文件目录
∙ \bullet ∙ 如果没有在源文件中放置 package 语句, 这个源文件中的类就被放置在一个默认包( defaulf package ) 中。
∙ \bullet ∙ 默认包是一个没有名字的包。
∙ \bullet ∙ 编译器将类文件也放在相同的目录结构中。- 例:将类分别放在不同的包中
( com. horstmann.corejava 和 com.mycompany)
从基目录编译和运行类,即包含 com 目录:
javac com/myconipany/Payrol1App.java
java com.mycompany.PayrollApp- 编译器javac: 编译.java文件,对文件(带有文件分隔符/ 和扩展名 .java 的文件)进行操作。
解释器java:加载类(带有 . 分隔符 )。
例:
▶ \blacktriangleright ▶ 包作用域
- 标记为 public 的部分可以被任意的类使用;
- 标记为 private 的部分只能被定义它们的类使用。
- 如果没有指定 public 或 private , 这个部分(类、方法或变量)可以被同一个包中的所有方法访问。即:默认为包可见。
- 密封包( package sealing) 机制
∙ \bullet ∙ 解决将各种包混杂在一起的问题。
∙ \bullet ∙ 如果将一个包密封起来, 就不能再向这个包添加类了。
第 3 章 Java 基本程序设计结构
3.1 Java 基础语法
▶ \blacktriangleright ▶ 运行机制
Java源文件 ⇒ 编译 j a v a c \xRightarrow{编译 javac} 编译javacJava字节码文件 ⇒ 运行 j a v a \xRightarrow{运行java } 运行java结果
▶ \blacktriangleright ▶ 开发注意事项和细节
- 类名:
对于所有的类来说,类名的首字母应该大写。如果类名由若干单词组成,那么每个单词的首字母应该大写,例如 MyFirstJavaClass 。- 方法名:
所有的方法名都应该以小写字母开头。如果方法名含有若干单词,则后面的每个单词首字母大写。- 主方法入口:
Java应用程序的执行入口是main()方法。它有固定的书写格式:
public static void main(String[]args)(…}- 编译后,每一个类都对应一个 .class 文件,可以直接运行不同的 .class文件。
例:
- 源文件名:
源文件名必须和类名相同。 一个源文件中最多只能有一个public类,其它类的个数不限。
也可以将main方法写在非public类中,然后指定运行非public类,这样入口方法就是非public的main方法。
3.1.1 基本数据类型
▶ \blacktriangleright ▶ Java 的两大数据类型:
- 内置基本数据类型
Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。- 引用数据类型
类、接口、数组
▶ \blacktriangleright ▶ 引用类型
- 在Java中,引用类型的变量非常类似于C/C++的指针。引用类型指向一个对象,指向对象的变量是引用变量。这些变量在声明时被指定为一个特定的类型,比如 Employee、Puppy 等。变量一旦声明后,类型就不能被改变了。
- 对象、数组都是引用数据类型。
- 所有引用类型的默认值都是null。
- 例:Site site = new Site(“Runoob”)。
▶ \blacktriangleright ▶自动类型转换
强制类型转换
- 条件是转换的数据类型必须是兼容的。
- 格式:(type)value
type是要强制类型转换后的数据类型
3.1.2 常量与变量
∙ \bullet ∙ 常量
▶ \blacktriangleright ▶ 常量
- 常量在程序运行时是不能被修改的。
- final 关键字:
在 Java 中使用 final 关键字来修饰常量
▶ \blacktriangleright ▶ 常量类型:
- 类常量(静态常量):
独立于方法之外的常量,用 static 修饰。
优点:可以在一个类中的多个方法中使用。
例:public static final double PI = 4.1415927- 成员常量(非静态常量):
独立于方法之外的常量,不过没有 static 修饰。
例:public final double PI = 4.1415927- 局部常量:
类的方法中的常量。
例:final double PI = 4.1415927;
∙ \bullet ∙ 变量
▶ \blacktriangleright ▶ 变量类型:
- 类变量(静态变量):
独立于方法之外的变量,用 static 修饰。
静态变量为类所有。- 实例变量或成员变量(非静态变量):
独立于方法之外的变量,不过没有 static 修饰。
实例变量为对象所有。- 局部变量:
类的方法中的变量。
1.类变量(静态变量)
- 声明::
类变量也称为静态变量,在类中以 static 关键字声明,但必须在方法之外。- 作用域:
★ \bigstar ★类变量可以被对象调用,还可以被类名调用- 作用周期:
静态变量在第一次被访问时创建,在程序结束时销毁。- 默认值:
默认值和实例变量相似。数值型变量默认值是 0,布尔型默认值是 false,引用类型默认值是 null。变量的值可以在声明的时候指定,也可以在构造方法中指定。此外,静态变量还可以在静态语句块中初始化。- 无论一个类创建了多少个对象,每个对象只拥有类变量的一份拷贝。
例:相当于每一个银行用户都对应相同的利率- ★ \bigstar ★ 本类访问类变量:
例:this.VariableName- ★ \bigstar ★ 其他类中访问类变量:
例:类名调用 className.VariableName
或:对象调用 ObjectReference.VariableName
3.实例变量或成员变量
- 声明::
独立于方法之外的变量,不过没有 static 修饰。- 作用域:
成员变量只能被对象调用,作用域是在对象中- 作用周期:
实例变量在对象创建的时候创建,在对象被销毁的时候销毁;- 默认值:
当一个对象被实例化之后,每个实例变量的值就跟着确定;
数值型变量默认值是 0,布尔型默认值是 false,引用类型默认值是 null。- 实例变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把实例变量设为私有。通过使用访问修饰符可以使实例变量对子类可见;
- ★ \bigstar ★ 本类的非静态方法中访问成员变量:
实例变量可以直接通过变量名访问。
例:this.VariableName- ★ \bigstar ★ 静态方法以及其他类中访问成员变量:
就应该使用完全限定名。
例:ObjectReference.VariableName
4.局部变量
- 声明::
在方法、构造方法或者语句块中;- 作用域:
只在声明它的方法、构造方法或者语句块中可见;- 作用周期:
局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,变量将会被销毁;- 默认值:
★ \bigstar ★局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用。- 访问修饰符不能用于局部变量;
- 局部变量是在栈上分配的。
3.1.3 运算符
运算符分类
- 算术运算符
加减乘除、取余、自增++、自减–- 关系运算符
- 位运算符
- 逻辑运算符
- 赋值运算符
- 条件运算符(?:)
3.1.4 流程控制
流程控制语句分类
- 顺序结构
- 分支结构 ( i f , s w i t c h ) (if,switch) (if,switch)
- 循环结构 ( f o r , w h i l e , d o . . w h i l e ) (for,while,do..while) (for,while,do..while)
3.2 数组
1.创建并初始化数组
- 声明数组的名字和类型;
- 创建数组;
- 初始化数组元素。
动态初始化:初始化时只指定数组长度,由系统为数组分配初始值
静态初始化:初始化时指定每个数组元素的初始值,由系统决定数组长度
- 数组元素的赋值与数组复制
数组引用的直接赋值不能实现数组复制,两个引用指向同一个地址
2.内存分配
Java程序在运行时,需要在内存中分配空间。为了提高运算效率,就对空间进行了不同区域的划分,因为每一片区域都有特定的处理教据方式和内存管理方式。
- 栈内存:存储局部变量
- 堆内存:存储new出来的内容(实体,对象);
每一个new出来的东西都有一个地址值;
使用完毕,会在垃圾回收器空闲时被回收。
3.起别名:引用的是同一个数组
数组名表示的是整个数组——如果我们将一个数组变量赋予另一个变量,那么两个变量将会指向同一个数组。
例如以下这段代码:
int[] a = new int[N];
a[i] = 1234;
int[] b = a;
b[i] = 5678;
//a[i]的值也会变成5678
第4章 对象与类
4.1 面向对象程序设计
编程方式: ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
- 步骤:
在面向对象程序设计时,首先从设计类开始,然后再往每个类中添加方法。- 如何设计复杂应用程序
需要的各种主力类( workhorse class)
通常,这些类没有 main 方法, 却有自己的实例域和实例方法。 要想创建 一个完整的程序, 应该将若干类组合在一起, 其中只有一个类有 main 方法。- 模块化编程
通过静态方法库 (模块) 实现了模块化编程。我们可以构造许多个静态方法库(模块),一个库中的静态方法也能够调用另一个库中定义的静态方法。
静态方法库:是定义在一个Java类中的一组静态方法。
类设计技巧
- 一定要保证数据私有
- 一定要对数据初始化
- 不要在类中使用过多的基本类型
- 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行分解
- 类名和方法名要能够体现它们的职责
- 优先使用不可变的类
4.1.1 对象
▶ \blacktriangleright ▶ 对象
- 三个主要特性:
对象的行为(behavior)—对对象施加的操作(方法)
对象的状态(state )—描述当前特征的信息(成员变量)
对象标识(identity )—对象的引用名- 构造器(constructor )
使用对象,就必须首先构造对象,由类构造(construct)对象的过程称为创建类的实例(instance)。
构造器与类同名;
构造器没有返回值;
构造器总是伴随着 new 操作一起调用;
构造器是一种特殊的方法, 用来构造并初始化对象。
不要在构造器中定义与实例域重名的局部变量。
例:new Date()
new Eraployee(“]ames Bond”, 100000, 1950, 1, 1)- ★ \bigstar ★类通常包括类型属于某个类 类型的实例域。
即:实例域本身就是对象,即类里面有其他类的实体
▶ \blacktriangleright ▶ 对象与对象变量 ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
- 对象变量:
希望构造的对象可以多次使用, 因此,需要将对象存放在一个变量中。
★ \bigstar ★ 任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new 操作符的返回值也是一个引用。- 对象与对象变量的重要区别
一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。- ★ \bigstar ★ 例:Date deadline = new Date();
表达式 new Date() 构造了一个 Date 类型的对象, 并且它的值是对新创建对象的引用。这个引用存储在变量 deadline 中。
4.1.2 类
属性 方法 构造器 代码块 内部类
▶ \blacktriangleright ▶类(class)
- 定义:
是构造对象的模板或蓝图。- 实例域:
对象中的数据(非静态的变量与常量)称为实例域( instance field )- 静态域
对象中的数据(静态的变量与常量)称为静态域- 方法
操纵数据的过程称为方法( method )- 绘制类图
▶ \blacktriangleright ▶类之间的关系
- 依赖(“ uses-a”):
一个类的方法操纵另一个类的对象。
应该尽可能地将相互依赖的类减至最少,让类之间的耦合度最小。- 聚合(“ has-a”):
聚合关系意味着类 A 的对象包含类 B 的对象。
例:订单类对象包含商品类对象- 继承(“ is-a”):
如果类 A 扩展类 B, 类 A 不但包含从类 B 继承的方法,还会拥有一些额外的功能。
类的成员
4.2 属性
1.静态域static :类变量 或 静态变量,每个对象是否共享该域
▶ \blacktriangleright ▶ static关键字
- 范围:
在Java类中,可用static修饰属性、方法、代码块、内部类- 修饰后的成员具备以下特点:
随着类的加载而加载
优先于对象存在
修饰的成员,被所有对象所共享
访问权限允许时,可不创建对象,直接被类调用
▶ \blacktriangleright ▶静态域:类变量 或 静态变量,每个对象是否共享该域
- 声明::
类变量也称为静态变量,在类中以 static 关键字声明,但必须在方法之外。- 调用:
★ \bigstar ★静态域为类所有,通过类来使用。 (
也可以通过对象来使用,提倡通过类名来使用,因为静态方法只要定义了类,不必建立类的实例就可使用。)- 作用周期:
静态变量在第一次被访问时创建,在程序结束时销毁。- 默认值:
默认值和实例变量相似。数值型变量默认值是 0,布尔型默认值是 false,引用类型默认值是 null。变量的值可以在声明的时候指定,也可以在构造方法中指定。此外,静态变量还可以在静态语句块中初始化。- 无论一个类创建了多少个对象,每个对象只拥有类变量的一份拷贝。
例:相当于每一个银行用户都对应相同的利率
- ★ \bigstar ★ 本类访问类变量: 在本类中,可以在任意方法、代码块、构造器中直接使用。
静态变量的get/set方法也静态的,当局部变量与静态变量重名时,使用“类名.静态变量
”进行区分。
例:VariableName- ★ \bigstar ★ 其他类中访问类变量:
例:类名调用 className.VariableName
或:对象调用 Obj.VariableName
public class Employee {
private static int total;//这里私有化,在类的外面必须使用get/set方法的方式来访问静态变量
static String company; //这里缺省权限修饰符,是为了方便类外以“类名.静态变量”的方式访问
private int id;
private String name;
}
public class TestStaticVariable {
public static void main(String[] args) {
//静态变量total的默认值是0
System.out.println("Employee.total = " + Employee.getTotal());
Employee e1 = new Employee("张三");
Employee e2 = new Employee("李四");
System.out.println(e1);//静态变量company的默认值是null
System.out.println(e2);//静态变量company的默认值是null
System.out.println("Employee.total = " + Employee.getTotal());//静态变量total值是2
Employee.company = "尚硅谷";
System.out.println(e1);//静态变量company的值是尚硅谷
System.out.println(e2);//静态变量company的值是尚硅谷
//只要权限修饰符允许,虽然不推荐,但是也可以通过“对象.静态变量”的形式来访问
e1.company = "超级尚硅谷";
System.out.println(e1);//静态变量company的值是超级尚硅谷
System.out.println(e2);//静态变量company的值是超级尚硅谷
}
}
2.实例域:实例变量 或 成员变量
▶ \blacktriangleright ▶实例域:实例变量 或 成员变量
- 声明::
独立于方法之外的变量,不过没有 static 修饰。- 调用:
∙ \bullet ∙ 成员变量只能被对象调用,作用域是在对象中
∙ \bullet ∙ 私有成员变量只能通过对象调用get\set()方法访问
∙ \bullet ∙ 父类私有成员变量继承后只能通过父类对象调用get\set()方法访问- 作用周期:
实例变量在对象创建的时候创建,在对象被销毁的时候销毁;- 默认值:
当一个对象被实例化之后,每个实例变量的值就跟着确定;
数值型变量默认值是 0,布尔型默认值是 false,引用类型默认值是 null。- 实例变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把实例变量设为私有。通过使用访问修饰符可以使实例变量对子类可见;
- ★ \bigstar ★ 本类的非静态方法中访问成员变量:
实例变量可以直接通过变量名访问。当局部变量与实例变量重名时,使用this.
区分
例:this.VariableName- ★ \bigstar ★ 静态方法以及其他类中访问成员变量:
就应该使用完全限定名。
例:ObjectReference.VariableName
子类中调用父类中同名的成员变量:起点不同(就近原则)局部、本类、父类
- 如果实例变量与局部变量重名
可以在实例变量前面加this.进行区别
- 如果子类实例变量和父类实例变量重名
并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量
- 如果父子类实例变量没有重名
只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问
class Father{
int a = 10;
int b = 11;
}
class Son extends Father{
int a = 20;
public void test(){
//子类与父类的属性同名,子类对象中就有两个a
System.out.println("子类的a:" + a);//20 先找局部变量找,没有再从本类成员变量找
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
//子类与父类的属性不同名,是同一个b
System.out.println("b = " + b);//11 先找局部变量找,没有再从本类成员变量找,没有再从父类找
System.out.println("b = " + this.b);//11 先从本类成员变量找,没有再从父类找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
public void method(int a, int b){
//子类与父类的属性同名,子类对象中就有两个成员变量a,此时方法中还有一个局部变量a
System.out.println("局部变量的a:" + a);//30 先找局部变量
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
System.out.println("b = " + b);//13 先找局部变量
System.out.println("b = " + this.b);//11 先从本类成员变量找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
}
class Test{
public static void main(String[] args){
Son son = new Son();
son.test();
son.method(30,13);
}
}
4.3 方法
▶ \blacktriangleright ▶方法(method):
- 定义:
是将具有独立功能的代码块组织成为一个整体,使其具有特殊功能的代码集- 公共方法
- 私有方法
对于私有方法, 如果改用其他方法实现相应的操作, 则不必保留原有的方法。
优点:只要方法是私有的,类的设计者就可以确信:它不会被外部的其他类操作调用,可以将其删去。如果方法是公有的, 就不能将其删去,因为其他的代码很可能依赖它
1.静态方法static
▶ \blacktriangleright ▶ 静态方法 ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
- 定义:
静态方法为类所有,静态方法是一种不能向对象实施操作的方法。
即:不使用任何对象,没有隐式的参数,不必建立类的实例就可使用。- 调用:
∙ \bullet ∙ 本类:通过静态方法名直接调用静态方法
∙ \bullet ∙ 外部类:可以通过“类名.静态方法“的方式调用,也可以通过”对象.静态方法“的方式调用
例:Math类的 pow方法:Math.pow(x, a)- 访问权限 ★ \bigstar ★
∙ \bullet ∙ 静态方法在访类的成员时:
只允许访问静态成员(即静态成员变量和静态方法)而不允许访问 实例成员变量 和 实例方法;
∙ \bullet ∙ 外部类访问静态方法时:
依赖于该静态方法访问权限。
- 访问权限原因: ★ \bigstar ★
因为实例成员变量是属于某个对象的,而静态方法在执行时,并不一定存在对象。同样,因为实例方法可以访问实例成员变量,如果允许静态方法调用实例方法,将间接地允许它使用实例成员变量,所以它也不能调用实例方法。
即:因为它不能操作对象,所以不能访问实列域,- 什么时候需要将方法声明为静态:两种情况下使用静态方法: ★ \bigstar ★
∙ \bullet ∙ 一个方法不需要访问对象状态(域),其所需参数都是通过显式参数提供。(例如:Math.pow)
∙ \bullet ∙ 一个方法只需要访问类的静态域
- 静态方法可以被子类继承,但不能被子类重写。 ★ \bigstar ★
静态方法是属于类的,父类和子类可以有同名类方法,由类调用,不由对象调用,与对象无关,与多态无关- 静态方法的调用都只看编译时类型。
- 面试题:是否可以从一个static方法内部发出对非static方法的调用?
特例main方法:
只能通过对象来调用非静态方法的。
本类中通过静态方法名直接调用静态方法
外部类依赖于静态方法访问权限
[修饰符] class 类{
[其他修饰符] static 返回值类型 方法名(形参列表){
方法体
}
}
使用空对象也能正常调用静态方法和静态域
/** * 判断如下程序运行时是否会报错? */
public class StaticTest {
public static void main(String[] args) {
Order order = null;
order.hello();
System.out.println(order.count);
}
}
class Order {
public static int count = 1;
public static void hello() {
System.out.println("hello!");
}
}
▶ \blacktriangleright ▶ 静态工厂方法 ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
- 定义:
不通过 new,而是用一个静态方法来对外提供自身实例,该静态方法称为静态工厂方法。- 第一优势:
它们有名字,对于代码的编写和阅读都能够更清晰。- 第二个优势:
不用每次被调用时都创建新对象,有时候外部调用者只需要拿到一个实例,而不关心是否是新的实例;- 第三个优势:
可以返回原返回类型的子类,构造方法只能返回确切的自身类型,而静态工厂方法则能够更加灵活,可以根据需要方便地返回任何它的子类型的实例。- 第四个优势:
在创建带泛型的实例时,能使代码变得简洁- 实际用途
∙ \bullet ∙ 可以有多个参数相同但名称不同的工厂方法
∙ \bullet ∙ 可以减少对外暴露的属性
调用方无须知道也无须指定 type 值,这样就能把 type 的赋值的范围控制住
∙ \bullet ∙ 多了一层控制,方便统一修改
同理, main 方法也是一个静态方法。
每一个类可以有一个 main 方法。这是一个常用于对类进行单元测试的技巧。
例:可以在 Employee 类中添加一个 main 方法用于单元测试。当运行整体程序时Employee 类的 main 方法永远不会执行。
- 总结
总体来说,考虑使用静态工厂方法代替构造器,除了有名字、可以用子类等这些语法层面上的优势之外,更多的是在工程学上的意义,它实质上的最主要作用是:能够增大类的提供者对自己所提供的类的控制力。
2.成员方法
▶ \blacktriangleright ▶ 方法调用
- 操作对象以及对象的属性。
- 实参:方法调用中的参数
- 形参:方法定义中的参数
等同于变量定义格式,例如:int number
▶ \blacktriangleright ▶ 参数传递方式 ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
- 基本数据类型:按值调用 (call by value)
对于基本数据类型的参数,形式参数的改变,不影响实际参数的值- ★ \bigstar ★ 引用数据类型(对对象中的数据而言):引用调用 ( call by reference)
∙ \bullet ∙ 对于引用类型的参数,形式参数的改变,影响实际参数的值。
∙ \bullet ∙ 例:方法操作引用变量中储存的 对象引用的 数据时,该对象的数据发生改变。
- ★ \bigstar ★ 对对象而言
Java 程序设计语言对对象采用的不是引用调用,实际上, 对象引用是按值传递的。
∙ \bullet ∙ 例:交换两个引用变量所储存的对象引用
∙ \bullet ∙ 结果:swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝。
在方法结束时参数变量 x 和 y 被丢弃了。原来的变量 a 和 b 仍然引用这个方法调用之前所引用的对象
- 总结 ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
∙ \bullet ∙ 一个方法不修改基本数据类型的实际参数(即数值型或布尔型)。
∙ \bullet ∙ 一个方法可以改变一个对象参数的状态。即:可以改变该引用数据类型对象的属性值
∙ \bullet ∙ 一个方法不能让对象参数引用一个新的对象。
即:不能通过方法使得一个引用类型的实参指向一个新的对象,因为对象引用是按值传递的
,将地址值赋值给一个引用类型形参,通过地址可以修改该引用对象的属性值,但不能改变实参的引用地址值
- 特别案例String:方法调用不改变实参的引用地址值,
且形参也不能通过引用地址修改该引用对象的属性值
因为String是不可变的,如果需要对字符串进行修改,就会产生新的String类对象,形参指向新的String类对象
★ \bigstar ★隐式参数与显式参数
- 隐式 ( implicit ) 参数:
是出现在方法名前的Employee 类对象。( 有些人把隐式参数称为方法调用的目标或接收者。)- 显式 ( explicit) 参数
位于方法名后面括号中的数值- 关键字 this :
在每一个方法中, 关键字 this 表示隐式参数。
这样可以将实例域与局部变量明显地区分开来。
3.方法重载:同类同名不同参
▶ \blacktriangleright ▶方法重载
方法重载指同一个类中定义的多个方法之间的关系,满足下列条件的多个方法相互构成重载
- 多个方法在同一个类中
- 多个方法具有相同的方法名
- 多个方法的参数不相同,类型不同或者数量不同
- 与返回值类型无关
即:也就是说, 不能有两个名字相同、 参数类型也相同却返回不同类型值的方法。
4.4 代码块 静态先行,由父及子,由普及构,就近原则
▶ \blacktriangleright ▶ 代码块
- 代码块(或初始化块)的作用:
对Java类或对象进行初始化- 静态代码块:类加载时执行
一个类中代码块若有修饰符,则只能被static修饰,称为静态代码块(static block),- 非静态代码块:随着对象创建而执行
没有使用static修饰的,为非静态代码块。先于构造器执行
实例变量
赋值顺序
▶ \blacktriangleright ▶ 初始化的过程:静态先行,由父及子
- 加载类:静态初始化
- 显示初始化\非静态初始化-构造器初始化
初始化的过程:加载类-(显示初始化\代码块初始化-构造器初始化)
class Root{
static{
System.out.println("Root的静态初始化块");
}
{
System.out.println("Root的普通初始化块");
}
public Root(){
System.out.println("Root的无参数的构造器");
}
}
class Mid extends Root{
static{
System.out.println("Mid的静态初始化块");
}
{
System.out.println("Mid的普通初始化块");
}
public Mid(){
System.out.println("Mid的无参数的构造器");
}
public Mid(String msg){
//通过this调用同一类中重载的构造器
this();
System.out.println("Mid的带参数构造器,其参数值:"
+ msg);
}
}
class Leaf extends Mid{
static{
System.out.println("Leaf的静态初始化块");
}
{
System.out.println("Leaf的普通初始化块");
}
public Leaf(){
//通过super调用父类中有一个字符串参数的构造器
super("尚硅谷");
System.out.println("Leaf的构造器");
}
}
public class LeafTest{
public static void main(String[] args){
new Leaf();
System.out.println();
new Leaf();
}
}
面试题:就近原则
/** * 阅读代码,分析运行结果 */
public class Test02 {
static int x, y, z;
static {
int x = 5; //局部变量x
x--; //就近原则,局部-本类-父类
}
static {
x--;
}
public static void method() {
y = z++ + ++z; //从左至右,先自增z=1,再运算
}
//0=-1+1 z=1
public static void main(String[] args) {
System.out.println("x=" + x); //-1
z--;//-1
method();
System.out.println("result:" + (z + y + ++z));//1+0+2
}
}
4.5 内部类
▶ \blacktriangleright ▶ 内部类
- 定义:
就是在一个类中定义一个类。
例:在一个类A的内部定义一个类B,类B就被称为内部类- 为什么需要使用内部类呢?
∙ \bullet ∙ 内部类方法可以访问该类定义所在的作用域中的数据, 包括私有的数据。
∙ \bullet ∙ 内部类可以对同一个包中的其他类隐藏起来。
∙ \bullet ∙ 当想要定义一个回调函数且不想编写大量代码时,使用匿名 (anonymous) 内部类比较便捷。- 内部类的访问特点
∙ \bullet ∙ 内部类可以直接访问外部类的所有成员,包括私有
∙ \bullet ∙ 外部类要访问内部类的成员,必须创建对象
分类:成员内部类、局部内部类
▶ \blacktriangleright ▶ 成员内部类:
- 直接声明在外部类的里面,在类的成员位置定义的类
- 分类:如果成员内部类中不使用外部类的非静态成员,那么通常将内部类声明为静态内部类,否则声明为非静态内部类。
- 调用:
格式:外部类名.内部类名 对象名 = 外部类对象.内部类对象;
范例:Outer.Inner oi = new Outer().new Inner();
▶ \blacktriangleright ▶ 局部内部类:声明在方法内、构造器内、代码块内的内部类
- 定义
局部内部类是在方法中定义的类,所以外界是无法直接使用。- 调用:
需要在方法内部创建对象并使用- 访问权限:
该类可以直接访问外部类的成员,也可以访问方法内的局部变量- 分类:
匿名的局部内部类
非匿名的局部内部类
1.匿名内部类
不用创建超类的具体的多个继承子类 或者创建接口的具体的多个实现类
只需要使用时创建不同的匿名子类对象\匿名实现类对象,实现不同的需求
,覆盖超类或接口中的方法。
▶ \blacktriangleright ▶ 匿名内部类:
- 本质定义:
是一个继承该类的匿名子类对象 或者实现该接口的匿名实现类的对象。
- 格式
new 类名或者接口名( ){
重写方法;
}- 应用:
可以不用创建超类的具体的多个继承子类 或者创建接口的具体的多个实现类
只需要使用时创建不同的匿名子类对象\匿名实现类对象,实现不同的需求
,覆盖超类或接口中的方法。
public class OuterClassTest2 {
public static void main(String[] args) {
SubA a = new SubA();
a.method();
//举例1:提供接口匿名实现类的对象
A a1 = new A(){
public void method(){
System.out.println("匿名实现类重写的方法method()");
}
};
a1.method();
//举例2:提供接口匿名实现类的匿名对象
new A(){
public void method(){
System.out.println("匿名实现类重写的方法method()");
}
}.method();
//举例3:
SubB s1 = new SubB();
s1.method1();
//举例4:提供了继承于抽象类的匿名子类的对象
B b = new B(){
public void method1(){
System.out.println("继承于抽象类的子类调用的方法");
}
};
b.method1();
System.out.println(b.getClass());
System.out.println(b.getClass().getSuperclass());
//举例5:
new B(){
public void method1(){
System.out.println("继承于抽象类的子类调用的方法1");
}
}.method1();
//举例6:
C c = new C();
c.method2();
//举例7:提供了一个继承于C的匿名子类的对象
C c1 = new C(){
};
c1.method2();
System.out.println(c1.getClass()); //OuterClassTest2的内部类
System.out.println(c1.getClass().getSuperclass()); //该匿名内部类的父类是C
C c2 = new C(){
public void method2(){
System.out.println("SubC");
}
};
c2.method2();
}
}
interface A{
public void method();
}
class SubA implements A{
@Override
public void method() {
System.out.println("SubA");
}
}
abstract class B{
public abstract void method1();
}
class SubB extends B{
@Override
public void method1() {
System.out.println("SubB");
}
}
class C{
public void method2(){
System.out.println("C");
}
}
匿名内部类:
/** * 匿名内部类使用: * 1.通过创建接口的具体的实现类,去覆盖超类或接口中的方法。 * 2.通过匿名内部类,去覆盖超类或接口中的方法。 */
public class JumpingDemo {
public static void main(String[] args) {
//接口操作类
JumpingOperator jo = new JumpingOperator();
//1.通过创建接口的具体的实现类,去覆盖超类或接口中的方法。
//1.1 直接对象调用
Jumping j = new Cat();
j.jump();
//1.2 通过接口操作类的方法调用
Jumping j2 = new Dog();
jo.method(j2);
System.out.println("------------------");
//2.通过匿名内部类,去覆盖Jumping接口中的jump方法。
//2.1 直接使用匿名内部类
new Jumping() {
@Override
public void jump() {
System.out.println("maoamo");
}
}.jump();
//2.1 通过方法使用匿名内部类
// void method(Jumping j):方法参数:接口的实现类对象,用匿名内部类代替
jo.method(new Jumping() {
@Override
public void jump() {
System.out.println("gougou");
}
});
}
}
第 5 章 类的封装、继承、多态
5.0 封装
- 为什么需要封装性?
理论上:
–高内聚
:类的内部数据操作细节自己完成,不允许外部干涉;
–低耦合
:仅暴露少量的方法给外部使用,尽量方便外部调用。
通俗的说:把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。- 如何实现数据封装?
Java规定了4种权限修饰,分别是:private、缺省、protected、public
我们可以使用4种权限修饰来修饰类及类的内部成员。当这些成员被调用时,体现可见性的大小。- 优点
封装给对象赋予了“ 黑盒” 特征, 这是提高重用性和可靠性的关键。 这意味着一个类可以全面地改变存储数据的方式,只要仍旧使用同样的方法操作数据, 其他对象就不会知道或介意所发生的变化。
6. ★ \bigstar ★基于类的访问权限???
一个方法可以访问所属类的所有对象的私有数据,而不仅限于访问隐式参数的私有特性。
- 如:
场景1:私有化(private)类的属性,提供公共(public)的get和set方法,对此属性进行获取或修改
场景2:将类中不需要对外暴露的方法,设置为private
场景3:单例模式中构造器private的了,避免在类的外部创建实例。(放到static关键字后讲)
★ \bigstar ★ 警告: 注意不要编写返回 可变对象的引用 的访问器方法。
- 例:Date 类有一个更改器方法 setTime()
即:getHireDay() 方法返回的 Date类 对象harry.hireDay ,可以不通过Employee类的类方法,而通过Date类的更改器方法 setTime()改变。
即:引用了一个可变的对象
- ★ \bigstar ★ 解决方式:(待学习)
如果需要返回一个可变对象的引用, 应该首先对它进行克隆(clone )。对象 clone 是指存放在另一个位置上的对象副本。
如果需要返回一个可变数据域的拷贝,就应该使用 clone。
5.1 继承:代码复用,继承属性,重写方法,子类重写父类super.
▶ \blacktriangleright ▶ 继承
- 定义:
继承已存在的类就是复用这些类的方法和域。- 公有继承
∙ \bullet ∙ 在 Java 中, 所有的继承都是公有继承, 而没有 C++ 中的私有继承和保护继承。
∙ \bullet ∙ java公有继承(public):
子类继承了父类的私有成员属性和私有成员方法,只是子类没有权限直接访问继承的父类的私有成员属性和私有成员方法
。 ★ \bigstar ★
∙ \bullet ∙ 如果子类的方法一定要访问私有域, 就必须借助于公有的接口;
即:子类中使用超类的 非private且能够访问超类私有域的方法,如:get\set()。 ★ \bigstar ★
使用超类的 对象调用超类私有成员方法。 ★ \bigstar ★- “ is-a” 规则:
∙ \bullet ∙ “ is-a” 关系是继承的一个明显特征,它表明子类的每个对象也是超类的对象。 例:每个经理都是雇员。
∙ \bullet ∙ “ is-a” 规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。
∙ \bullet ∙ 即:子类对象可以替换超类对象
∙ \bullet ∙ 如:用正方形替换四边形- 子类中可以增加域、 增加方法或覆盖超类的方法,然而绝对不能删除继承的任何域和方法。
- 多继承
∙ \bullet ∙ Java 不支持多继承
∙ \bullet ∙ Java 中多继承功能的实现方式
▶ \blacktriangleright ▶ 定义子类
- 关键字 extends
∙ \bullet ∙ 表明正在构造的新类派生于一个已存在的类。
∙ \bullet ∙ 已存在的类称为超类( superclass)、 基类( base class) 或父类(parent class);
∙ \bullet ∙ 新类称为子类(subclass、) 派生类( derived class) 或孩子类(child class)。- 例:由继承 Employee 类来定义 Manager 类的格式
public class Manager extends Employee
{
添加方法和域
}
▶ \blacktriangleright ▶ 子类构造器
- ★ \bigstar ★ 子类对象初始化:
由于子类的构造器不能访问超类的私有域, 所以必须利用超类的构造器对子类中继承的来的私有域进行初始化- ★ \bigstar ★ 子类中如何使用超类的构造器:
super
关键字
∙ \bullet ∙ 使用关键字 super调用超类的构造器。
∙ \bullet ∙ 使用super 调用构造器的语句必须是子类构造器的第一条语句。
∙ \bullet ∙ 如果子类的构造器没有显式地调用超类的构造器, 则将自动地调用超类默认(没有参数 )的构造器。
∙ \bullet ∙ 如果超类没有不带参数的构造器, 并且在子类的构造器中又没有显式地调用超类的其他构造器,则 Java 编译器将报告错误。
例:
public Manager(String name, double salary, int year, int month, int day)
{
super(name, salary, year, month, day);//调用超类 Employee 中含有 n、s、year month 和 day 参数的构造器
bonus = 0;
}
1. 重写override\overwrite:基同引兼同名参,psf不重写
▶ \blacktriangleright ▶ 子类的覆盖方法:同名同参,返回类型兼容
- 定义:
∙ \bullet ∙ 超类中的有些方法对子类并不一定适用,为此,需要提供一个新的方法来覆盖(override)超类中的这个方法。- 实现覆盖
∙ \bullet ∙ 子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
∙ \bullet ∙ 声明为 final 的方法不能被重写。
∙ \bullet ∙ 声明为 static 的方法不能被重写。
静态方法是属于类的,子类可以有同名类方法
,由类调用,不由对象调用,与对象无关,与多态无关
∙ \bullet ∙ 声明为 private的方法不能被重写。
子类可以有同名方法,是子类自己的新方法,由子类对象调用
即:不能直接访问父类私有方法,只能通过父类对象调用,都不能用,你怎么重写- 警告:
在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。
- 允许子类将覆盖方法的返回类型 定义 为原返回类型的子类型
- 跨包的父类缺省的方法也不能重写
即:父类缺省的方法本包可见,外部类不能直接访问,都不能用,你怎么重写
2. 关键字 this
与super
就近原则
▶ \blacktriangleright ▶ 关键字
this
:两使用
- 实例方法或构造器中:使用当前对象的成员变量,隐式参数
用this来区分成员变量
和局部变量
- 构造器中:同一个类中构造器互相调用
∙ \bullet ∙ 调用当前类的其他的构造器:
this(参数)必须声明在当前构造器的首行,this(参数)在构造器中最多声明一个
例:调用同一个类的另一个构造器
注意:
- 构造器间不能出现递归调用:
推论:如果一个类中声明了n个构造器,则最多有 n – 1个构造器中使用了”this(形参列表)”- this()和this(实参列表)只能声明在构造器首行。
推论:在类的一个构造器中,最多只能声明一个”this(参数列表)”
▶ \blacktriangleright ▶ 关键字
super
:3使用
- 子类中调用父类被重写的方法
如果子类重写了父类的方法,则子类对象初始化和方法执行默认调用子类重写的方法 ★ \bigstar ★
在子类中需要通过super.
才能调用父类被重写的方法- 子类中调用父类中同名的成员变量
同名变量的确定原则:就近原则
先找局部变量找,没有再从本类成员变量找,没有再从父类找 ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
★ \bigstar ★this.
和super.
决定查找起点
★ \bigstar ★ 查找顺序:局部、本类、父类- 子类构造器中调用父类构造器
★ \bigstar ★ 子类对象初始化:由于子类的构造器不能访问超类的私有域, 所以必须利用超类的构造器对子类中继承的来的私有域进行初始化
∙ \bullet ∙ 子类继承父类时,不会继承父类的构造器。只能通过super(形参列表)
的方式调用父类指定的构造器。
∙ \bullet ∙ 规定:“super(形参列表)”,必须声明在构造器的首行。
∙ \bullet ∙ 如果在子类构造器的首行既没有显示调用”this(形参列表)“,也没有显式调用”super(形参列表)”,则子类此构造器默认调用”super()”
即:默认调用父类中空参的构造器。若父类中没有空参的构造器,则编译出错
。 ★ \bigstar ★
总结 ★ \bigstar ★
- 结论:在构造器的首行,“this(形参列表)” 和 “super(形参列表)”只能二选一。
- 结论:子类的任何一个构造器中,要么通过
this()
调用本类中重载的构造器,要么通过super()
或 默认调用父类的构造器。
只能是这两种情况之一。- 结论:一个类中声明有n个构造器,最多有n-1个构造器中使用了”this(形参列表)“,则剩下的那个一定使用”super(形参列表)”。
▶ \blacktriangleright ▶ 子类中调用父类被重写的方法步骤
多态方法 的调用步骤
设要调用 x.f(args),隐式参数 x 声明为类 C 的一个对象。
- 编译器査看对象的声明类型和方法名:编译器获得所有可能被调用的候选方法。
编译器将会一一列举所有本类中名为 f 的方法和其超类中访问属性为 public 且名为 f 的方法(超类的私有方法不可访问)。- 重载解析( overloadingresolution):根据参数类型确定方法
编译器将査看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。
1.子类中调用父类被重写的方法
- 如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法;
- 如果子类重写了父类的方法,在子类中需要通过`super.`才能调用父类被重写的方法,否则默认调用的子类重写的方法
--------------------------------------------------------------------------------
2.子类中调用父类中同名的(局部、子类、父类)成员变量:起点不同(就近原则)局部、本类、父类
- 如果实例变量与局部变量重名
可以在实例变量前面加this.进行区别
- 如果子类实例变量和父类实例变量重名
并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量
- 如果父子类实例变量没有重名
只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问
class Father{
int a = 10;
int b = 11;
}
class Son extends Father{
int a = 20;
public void test(){
//子类与父类的属性同名,子类对象中就有两个a
System.out.println("子类的a:" + a);//20 先找局部变量找,没有再从本类成员变量找
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
//子类与父类的属性不同名,是同一个b
System.out.println("b = " + b);//11 先找局部变量找,没有再从本类成员变量找,没有再从父类找
System.out.println("b = " + this.b);//11 先从本类成员变量找,没有再从父类找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
public void method(int a, int b){
//子类与父类的属性同名,子类对象中就有两个成员变量a,此时方法中还有一个局部变量a
System.out.println("局部变量的a:" + a);//30 先找局部变量
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
System.out.println("b = " + b);//13 先找局部变量
System.out.println("b = " + this.b);//11 先从本类成员变量找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
}
class Test{
public static void main(String[] args){
Son son = new Son();
son.test();
son.method(30,13);
}
}
--------------------------------------------------------------------------------
3.子类构造器中调用父类构造器
例3:
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
B(){
System.out.println("B类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类显示声明一个无参构造,
//B类的无参构造中虽然没有写super(),但是仍然会默认调用A类的无参构造
//可以看到会输出“A类无参构造器"和"B类无参构造器")
}
}
例6:
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
super();
System.out.println("B类无参构造器");
}
}
class Test06{
public static void main(String[] args){
B b = new B();
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个无参构造,
//B类的无参构造明确写super(),表示调用A类的无参构造
//编译报错,因为A类没有无参构造
}
}
例7:
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(int a){
super(a);
System.out.println("B类有参构造器");
}
}
class Test07{
public static void main(String[] args){
B b = new B(10);
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个有参构造,
//B类的有参构造明确写super(a),表示调用A类的有参构造
//会打印“A类有参构造器"和"B类有参构造器"
}
}
面试题:静态先行,由父及子,由普及构;子类重写,父类super.
代码块:静态先行,由父及子,由普及构
继承:子类重写,父类super.
public class Test03 {
public static void main(String[] args) {
Sub s = new Sub();
}
}
class Base{
Base(){
method(100);
}
{
System.out.println("base");
}
public void method(int i){
System.out.println("base : " + i);
}
}
class Sub extends Base{
Sub(){
super.method(70);
}
{
System.out.println("sub");
}
public void method(int j){
System.out.println("sub : " + j);
}
}
3. 访问修饰符
▶ \blacktriangleright ▶ 类的访问修饰符:本包,跨包
- 跨包public:表示任意位置都可以访问该类;
- 本包缺省:默认访问修饰符,即在同一个包中可以访问;
▶ \blacktriangleright ▶ 成员的访问修饰符:本类,本包,跨包子类,任意位置
- private:本类
- 缺省:本包
- protected:跨包子类
即:导包后其他包中子类可以访问。- public:模块任意位置
即:导包后其他包中类可以访问。
5.2 多态:对象多态不一致
对象的多态性,即一个引用类型变量可以指向(引用)多种不同类型的对象
引用变量的编译时类型和运行时类型不一致,就出现了对象的多态性,
属性不满足多态性,方法满足多态性
1.对象的多态 ( polymorphism): 方法多态
▶ \blacktriangleright ▶ 1.多态:对象的多态性
- 对象的多态:
∙ \bullet ∙ 在Java中,父类的引用指向子类的对象
也即:子类对象可以替代父类对象使用。
∙ \bullet ∙ 一个引用类型变量可以指向(引用)多种不同类型的对象
即:一个超类引用变量既可以引用一个超类对象, 也可以引用该类的任何一个子类的对象。
如:
∙ \bullet ∙ 多态的形式:类多态,接口多态。
∙ \bullet ∙ 多态的前提:有继承或者实现关系;有方法重写;即:有父(类/接口)引用 指向(子/实现)类对象
- 动态绑定
调用的方法依赖于隐式参数的实际类型,在运行时能够自动地选择调用哪个方法的现象称为动态绑定( dynamic binding)- 静态绑定( static binding ):
如果是 private 方法、 static 方法、 final 方法或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称为静态绑定。属性不满足多态性,属性是在编译时确定的
方法满足多态性,方法是在运行时确定的
运行时确定引用变量类型,继而确定调用的方法
▶ \blacktriangleright ▶ 2.引用变量的
编译时类型
和运行时类型
:编译时,看左边;运行时,看右边。
- Java引用变量有两个类型:
编译时类型
和运行时类型
。
∙ \bullet ∙ 编译时类型 由声明该变量时使用的类型决定
∙ \bullet ∙ 运行时类型 由实际赋给该变量的对象决定。- 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
▶ \blacktriangleright ▶ 3.多态使用和好处
- 方法内局部变量的赋值体现多态
即:父类引用变量引用子类的对象。- 方法的形参声明为父类类型体现多态,减少大量的重载方法的定义
即:形参是父类类型,实参是子类对象
例: public boolean equals(Object obj)- 方法返回值类型是父类类型体现多态
即:方法返回值类型是父类类型,返回值是子类对象
public class PetShop {
//返回值类型是父类类型,实际返回的是子类对象
public Pet sale(String type){
switch (type){
case "Dog":
return new Dog();
case "Cat":
return new Cat();
}
return null;
}
}
2.向上转型与向下转型:向上多态性,向下调特有
▶ \blacktriangleright ▶ 首先,一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型不会变。 但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。
▶ \blacktriangleright ▶ 向上转型:
当左边的变量的类型(父类)>右边对象/变量的类型(子类),我们就称为向上转型。
- 此时,编译时按照左边变量的类型处理,就
只能调用父类中有的变量和方法
,不能调用子类特有的变量和方法了。- 但是,
运行时
,仍然是对象本身的类型,所以执行的方法
是子类重写的方法体。属性不满足多态性,属性是在编译时确定的
- 此时,一定是安全的,而且也是自动完成的
▶ \blacktriangleright ▶ 向下转型:编译时,看左边;运行时,看右边。
向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型。只有在超类对象使用子类中特有的方法时才需要进行类型转换
- 此时,
编译时
按照左边变量的类型处理,就可以调用子类特有的变量和方法
- 但是,
运行时
,仍然是对象本身的类型。- 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过
instanceof
关键字进行判断
向下转型:
public class ClassCastTest {
public static void main(String[] args) {
//没有类型转换
Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog
//向上转型
Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dog
pet.setNickname("小白");
pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
// pet.watchHouse();//不能调用父类没有的方法watchHouse
Dog d = (Dog) pet;
变量d的编译时类型是Dog
变量pet的运行时类型是Dog,则变量d的运行时类型是Dog
System.out.println("d.nickname = " + d.getNickname());
d.eat();//可以调用eat方法
d.watchHouse();//可以调用子类扩展的方法watchHouse
Cat c = (Cat) pet;
编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
}
}
instanceof关键字:检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A
如果对象a属于类A或类A的子类,则a instanceof A值为true。
public class TestInstanceof {
public static void main(String[] args) {
Pet[] pets = new Pet[2];
pets[0] = new Dog();//多态引用
pets[0].setNickname("小白");
pets[1] = new Cat();//多态引用
pets[1].setNickname("雪球");
for (int i = 0; i < pets.length; i++) {
pets[i].eat();
if(pets[i] instanceof Dog){
Dog dog = (Dog) pets[i];
dog.watchHouse();
}else if(pets[i] instanceof Cat){
Cat cat = (Cat) pets[i];
cat.catchMouse();
}
}
}
}
3.属性不满足多态性,属性是在编译时确定的
class Base {
int count = 10;
public void display() {
System.out.println(this.count);
}
}
class Sub extends Base {
int count = 20;
public void display() {
System.out.println(this.count);
}
}
public class FieldMethodTest {
public static void main(String[] args){
Sub s = new Sub();
System.out.println(s.count);//20
s.display();//20
Base b = s;
System.out.println(b == s); //true
System.out.println(b.count);//10,属性不满足多态性,b是Base类的对象,
b.display();//20
Base b1 = new Base();
System.out.println(b1.count); //10
b1.display();//10
}
}
4.final:不继承、不重写、不修改
▶ \blacktriangleright ▶ 阻止继承:final 类和 final 方法
- final 类
∙ \bullet ∙ 不允许扩展的类被称为 final 类。表示该类不能被子类继承,该类即为最终类,不可再被继承。
∙ \bullet ∙ final 类中的所有方法自动地成为 final 方法 。
∙ \bullet ∙ 只有其中的方法自动地成为 final,而不包括域。
例:使用 final 修饰符声明
public final class Executive extends Manager
{…
}- final方法
子类不能覆盖这个方法。- final 域:
构造对象之后就不允许改变它们的值- 例:String 类也是 final 类
这意味着不允许任何人定义 String 的子类。
即:如果有一个 String 的引用, 它引用的一定是一个 String 对象, 而不可能是其子类的对象(因为没有子类)。
▶ \blacktriangleright ▶ final 实例域 ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
- 规则:
构建对象时必须初始化这样的域,初始化后不能够再对它进行修改。- 应用:
final 修饰符大都应用于基本 (primitive ) 类型域,或不可变(immutable) 类的域。
(不可变类:类中的每个方法都不会改变其对象,例如:String类就是一个不可变的类)
★ \bigstar ★ 即:应用于 基本数据类型 和 引用数据类型(不可变类的对象)
例:基本数据类型
public final double PI = 4.1415927
例:不可变类对象的引用数据类型
private final String name;
即:不可变类对象属性值不可变,final修饰String类的对象 引用地址值不变
没有final修饰String类对象的引用地址值可变
★ \bigstar ★- ★ \bigstar ★ 对于可变的类的对象使用 final 修饰符
例:private final StringBuiIcier evaluations
final 关键字只是表示存储在引用变量中的对象引用不会再指示其他 StringBuilder对象。不过这个对象可以更改:即对象引用地址值不变,属性值可变 ★ \bigstar ★
面试题:即对象引用地址值不变,属性值可变
题目1:排错
public class Something {
public int addOne(final int x) {
return ++x;
// return x + 1;
}
}
题目2:排错
public class Something {
public static void main(String[] args) {
Other o = new Other();
new Something().addOne(o);
}
public void addOne(final Other o) {
o = new Other(); //错:表示对象o的引用地址值不可变,属性值i可变
o.i++;
}
}
class Other {
public int i;
}
5.3 Object 类
▶ \blacktriangleright ▶ Object类:所有类的超类
- 定义:
Object 类是 Java 中所有类的始祖, 在 Java 中每个类都是由它扩展而来的。- 默认声明:
如果没有明确地指出超类,Object 就被认为是这个类的超类。- Object的对象变量
∙ \bullet ∙ 可以使用 Object 类型的变量引用任何类型的对象:
∙ \bullet ∙ Object 类型的变量只能用于作为各种子类对象值的通用持有者,要想对其中的内容进行具体的操作, 还需要清楚对象的原始类型, 并进行相应的类型转换:
∙ \bullet ∙ 例:
Object obj = new Employee(‘Harry Hacker”, 35000);
Employee e = (Employee) obj ;- 所有的数组类型,不管是对象数组还是基本类型的数组都扩展了 Object 类。
∙ \bullet ∙ 例:
Employee[] staff = new Employee[10];
obj = staff; // OK
obj = new int[10]; // OK
1.equals 方法
自动生成 Alt+insert
▶ \blacktriangleright ▶ equals 方法:比较引用数据类型,是否重写
- 定义:
Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象,这个方法将判断两个对象是否具有相同的引用。
★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
- 基本类型比较值:只要两个变量的值相等,即为true。
- 引用类型比较引用(是否指向同一个对象):只有指向同一个对象时,==才返回true。即:两个引用的地址相同
- 比较一个类的两个对象内容是否相同:对于该类的每一个引用数据类型的属性,都需要重写该属性的equal()方法
类File、String、Date及包装类(Wrapper Class)已经重写了Object类的equals()方法
比较一个类的两个对象内容是否相同:该类的每一个引用数据类型的属性,都需要重写该属性的equal()方法
public class CustomerTest {
public static void main(String[] args) {
Customer c1 = new Customer("Tom",12,new Account(2000));
Customer c2 = new Customer("Tom",12,new Account(2000));
System.out.println(c1.equals(c2));//false -->true
}
}
▶ \blacktriangleright ▶ toString 方法
- 定义:
∙ \bullet ∙ 它用于返回表示对象值的字符串。
∙ \bullet ∙ Object 类定义了 toString 方法, 用来打印输出对象所属的类名和散列码。- 调用方式
∙ \bullet ∙ 通过对象调用。
例:x.toString( )
∙ \bullet ∙ 通过操作符:
只要对象与一个字符串通过操作符“ +” 连接起来,Java 编译就会自动地调用 toString方法。
即:调用 x.toString( ) 的地方可以用 “”+x 替代- 覆盖的 toString方法
∙ \bullet ∙ 格式:类的名字,随后是一对方括号括起来的域值。
例:Manager[name=…,salary=…,hireDay=…][bonus=…]- 打印数组
∙ \bullet ∙ 数组继承了 object 类的 toString 方法,数组类型将按照Object类的toString 方法的格式打印。
∙ \bullet ∙ 解决方式:调用静态方法 Arrays.toString
∙ \bullet ∙ 打印多维数组:需要调用 Arrays.deepToString 方法。- 建议:
强烈建议为自定义的每一个类增加 toString 方法。这样做不仅自己受益, 而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。
▶ \blacktriangleright ▶ getClass 方法
- 定义:获取对象的运行时类型
∙ \bullet ∙返回一个 Class 类型的实例。
一个Class 对象将表示一个特定类的属性
∙ \bullet ∙返回包含对象信息的类对象。
∙ \bullet ∙返回此Object的运行时类。 返回的类对象是由所表示的类的static synchronized方法锁定的对象。- 用途
通过getClass().getName() 获得类名
finalize():Object中定义,回收时调用,执行必要的操作,已过时
5.4 抽象类\ 抽象方法: abstract
▶ \blacktriangleright ▶抽象类
- 抽象类不能实例化。
即:不能new一个抽象类对象,因为创建对象后,调用抽象的方法,没有意义。- 抽象类多态:可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。
∙ \bullet ∙ 即:可以声明一个抽象类的对象变量,用于引用非抽象子类的对象。
例:Person p = new Student(“Vinee Vu” , “Economics”);- 类即使不含抽象方法,也可以将类声明为抽象类。
- 建议:
★ \bigstar ★ 尽量将通用的域和方法(不管是否是抽象的)放在超类(不管是否是抽象类)中。
- 抽象方法不允许被调用:
方法调用需要使用类名或对象,进而推出不允许抽象类实列化对象,不能把静态方法抽象化
▶ \blacktriangleright ▶ abstract 关键字
- 抽象方法:
不需要在抽象类中具体实现
∙ \bullet ∙ 例:public abstract String getDescription();- 抽象类:
∙ \bullet ∙ 表示该类为一个抽象类,不能实例化该类;
∙ \bullet ∙ 包含一个或多个抽象方法的类。除了抽象方法之外,抽象类还可以包含具体数据和具体方法。
∙ \bullet ∙ 例:
public abstract class Person
{
public abstract String getDescription();
}
▶ \blacktriangleright ▶ 继承抽象类
两种方式:
- 在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;
- 定义全部的抽象方法,这样子类就不是抽象的了。
5.5 枚举类enum:静态常量对象、属性、构造器
▶ \blacktriangleright ▶ 枚举类型
- 定义:枚举类型本质上也是一种类,只不过是这个类的对象是有限的、固定的几个,不能让用户随意创建。
- 声明:
enum 关键字
- 类常量的对象、对象的实例变量、构造器
各个常量的对象使用逗号 ”,“来分割。必须在枚举类的首行,因为是常量,所以建议大写。
▶ \blacktriangleright ▶ enum中常用方法
实现接口的枚举类
//1、枚举类实现的抽象方法:
//可以像普通的类一样,实现接口,并且可以多个,但要求必须实现里面所有的抽象方法!
enum A implements 接口1,接口2{
//抽象方法的实现
}
//2、枚举类的静态常量对象实现接口抽象方法!
enum A implements 接口1,接口2{
常量名1(参数){
//抽象方法的实现或重写
},
常量名2(参数){
//抽象方法的实现或重写
},
//...
}
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;
}
5.6 包装类:
▶ \blacktriangleright ▶ 对象包装器
- 定义:
∙ \bullet ∙ 将基本类型转换为对象。 所有的基本类型都有一个与之对应的类。这些类称为包装器 ( wrapper )
∙ \bullet ∙ 对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。
∙ \bullet ∙ 对象包装器类还是 final类 , 因此不能定义它们的子类。
- 应用:
∙ \bullet ∙ 数组列表的泛型数据类型只能为引用数据类型。
即:不允许写成 ArrayList< int>。
∙ \bullet ∙ 使用包装器类声明数组列表。
例:使用 Integer对象包装器类声明一个 Integer对象的数组列表。
ArrayList< Integer> list = new ArrayList<>();
1.基本数据类型 与 包装类之间的转换
▶ \blacktriangleright ▶ 基本数据类型 与 包装类之间的转换
- 自动装箱(autoboxing)
∙ \bullet ∙ 便于添加 int 类型的元素到 ArrayLis< lnteger>()中。
例:list.add(3); ⇒ 自动变换 \xRightarrow{自动变换} 自动变换 list.add (Integer.value0f(3));- 自动拆箱
∙ \bullet ∙ 当将一个 Integer对象赋给一个 int 值时,将会自动地拆箱
例:int n = list.get(i); ⇒ 自动变换 \xRightarrow{自动变换} 自动变换 int n = list.get(i).intValue();
∙ \bullet ∙ 在算术表达式中也能够自动地装箱和拆箱
编译器将自动地插人一条对象拆箱的指令, 然后进行自增计算, 最后再将结果装箱。
例:Integer n = 3; n++;- ★ \bigstar ★自动装箱规范
要求 boolean、byte、char <128, short 和int 介于 -128 ~ 127 之间的 被包装到固定的对象中。
2.基本数据类型\包装类 ⇔ \lrArr ⇔ 字符串
转成谁就用谁的valueOf()方法来转
- 转为字符串:
String.valueOf(基本数据类型\包装类对象a);包装类.valueOf(“字符串str”)
:转换为对应的包装类型
Integer.valueOf(“字符串str”)
Double.valueOf(“字符串str”);包装类.parseXxx( String str)
除了Character类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型
3.包装类缓存对象:共享的包装类对象
▶ \blacktriangleright ▶ 包装类缓存对象
包装类缓存对象:
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
Double d1 = 1.0;
Double d2 = 1.0;
System.out.println(d1==d2);//false 比较地址,没有缓存对象,每一个都是新new的
4.数值对比:有基就自动,无基就报错
5.7 泛型数组列表
▶ \blacktriangleright ▶ 数组列表
- 定义:
∙ \bullet ∙ ArrayList类 是一个采用类型参数( type parameter ) 的泛型类( generic class)
∙ \bullet ∙ ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,可以添加或删除元素。
∙ \bullet ∙ 注释: 在 Java 的老版本中, 程序员使用 Vector 类实现动态数组。 不过, ArrayList 类更加有效,没有任何理由一定要使用 Vector 类。- 声明
∙ \bullet ∙ ArrayList< E> objectName =new ArrayList<>();
∙ \bullet ∙ E: 泛型数据类型, ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★只能为引用数据类型,用于设置 objectName 的数据类型。- add 方法
∙ \bullet ∙ 使用 add 方法可以将元素添加到数组列表中。
∙ \bullet ∙ 如果调用 add 且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。- ensureCapacity 方法
∙ \bullet ∙如果已经清楚或能够估计出数组可能存储的元素数量, 就可以在填充数组之前调用ensureCapacity方法;
例:staff.ensureCapacity(lOO);
∙ \bullet ∙还可以把初始容量传递给 ArrayList 构造器。
例:ArrayList staff = new ArrayListo(lOO);- trimToSize方法
∙ \bullet ∙ 一旦能够确认数组列表的大小不再发生变化,就可以调用 trimToSize方法。
∙ \bullet ∙ 将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间.
5.9 继承的设计技巧
- 将公共操作和域放在超类
这就是为什么将姓名域放在 Person类中,而没有将它放在 Employee 和 Student 类中的原因。- 不要使用受保护的域
∙ \bullet ∙ 子类集合是无限制的, 任何一个人都能够由某个类派生一个子类,并编写代码以直接访问 protected 的实例域, 从而破坏了封装性。
∙ \bullet ∙ 在 Java 程序设计语言中,在同一个包中的所有类都可以访问 proteced 域,而不管它是否为这个类的子类。
∙ \bullet ∙ protected 方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。- 使用继承实现“ is-a” 关系
使用继承很容易达到节省代码的目的,但有时候也被人们滥用了。- 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期的行为
- 使用多态, 而非类型信息
- 不要过多地使用反射
第6章 接口、lambda 表达式
6.1 接口
1.接口概念
▶ \blacktriangleright ▶ 接口概念
- 接口就是规范,定义的是一组规则
声明接口关键字:interface
public interface 接口名()- 实现类:
implements
public class 类名 implements 接口名{}- 多实现
一个类是可以实现多个接口的,必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。- 多继承(extends)
一个接口能继承另一个或者多个接口
▶ \blacktriangleright ▶ 接口多态
- 接口不能实例化
- 接口的实现类
∙ \bullet ∙ 非抽象类:必须重写接口中的所有抽象方法
∙ \bullet ∙ 否则为抽象类,抽象类不可以实例化对象
- 接口多态
接口类型的引用变量 引用 实现类的对象,构成多态引用。
接口多态:
public class VehicleTest {
public static void main(String[] args) {
Developer developer = new Developer();
//创建三个交通工具,保存在数组中
Vehicle[] vehicles = new Vehicle[3];
vehicles[0] = new Bicycle("捷安特","骚红色");
vehicles[1] = new ElectricVehicle("雅迪","天蓝色");
vehicles[2] = new Car("奔驰","黑色","沪Au888");
for (int i = 0;i < vehicles.length;i++){
developer.takingVehicle(vehicles[i]);
//对象a instanceof 接口A:如果对象a属于接口A的实现类,则a instanceof A值为true。
if(vehicles[i] instanceof IPower){
((IPower) vehicles[i]).power();//接口多态:调用 接口实现类对象 实现的方法。
}
}
}
}
接口实现类的对象:实现类的对象、实现类的匿名对象、匿名实现类的对象、匿名实现类的匿名对象
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("扫描仪结束工作");
}
});
}
}
2.接口的成员
▶ \blacktriangleright ▶ 接口的成员
- 静态常量
因为接口是标准规范,那么在规范中需要声明一些底线边界值,当实现者在实现这些规范时,不能去随意修改和触碰这些底线,否则就有“危险”。
默认修饰符:public static final- 抽象方法:抽象不可调,抽象必实现
默认修饰符:public abstract
JDK8和JDK9中:接口作为一个抽象标准定义的概念。
- 默认方法:
default
∙ \bullet ∙ 添加新方法:我们要在已有的老版接口中提供新方法时,如果添加抽象方法,就会涉及到原来使用这些接口的类就会有问题,那么为了保持与旧版本代码的兼容性,只能允许在接口中定义默认方法实现。
比如:Java8中对Collection、List、Comparator等接口提供了丰富的默认方法。
∙ \bullet ∙ 方法复用:当我们接口的某个抽象方法,在很多实现类中的实现代码是一样的,此时将这个抽象方法设计为默认方法更为合适,那么实现类就可以选择重写,也可以选择不重写。- 静态方法
因为之前的标准类库设计中,有很多Collection/Colletions或者Path/Paths这样成对的接口和类,后面的类中都是静态方法,而这些静态方法都是为前面的接口服务的,那么这样设计一对API,不如把静态方法直接定义到接口中使用和维护更方便。- 私有方法
▶ \blacktriangleright ▶ 使用接口的成员
- 静态常量、静态方法
直接使用“接口名.
”进行调用,不能通过实现类的对象进行调用- 非静态:抽象方法、默认方法
只能通过实现类对象才可以调用
接口不能直接创建对象,只能创建实现类的对象
[修饰符] interface 接口名{
公共的静态常量
公共的抽象方法
公共的默认方法(JDK1.8以上)
公共的静态方法(JDK1.8以上)
私有方法(JDK1.9以上)
}
3.默认方法
▶ \blacktriangleright ▶ 解决默认方法冲突规则
- 超类优先:
如果超类提供了一个具体方法,同名而且有相同参数类型的接口默认方法会被忽略。- 接口冲突:
如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法, 必须覆盖这个方法来解决冲突
接口默认方法与类的具体成员方法冲突:
重写用重写,默认调默法;冲突必实现,原则类优先;父类用super.,接口用interface.super
默认方法与抽象方法,接口与实现类;接口与接口,接口与类;父类具体方法与接口默认方法
package com.atguigu08._interface.jdk8;
public class SubClassTest {
public static void main(String[] args) {
//知识点1:接口中声明的静态方法只能被接口来调用,不能使用其实现类进行调用。
CompareA.method1();
// SubClass.method1();
//知识点2:接口中声明的默认方法可以被实现类继承,实现类在没有重写此方法的情况下,
//默认调用接口中声明的默认方法。如果实现类重写了此方法,则调用的是自己重写的方法。
SubClass s1 = new SubClass();
s1.method2();
//知识点3:类实现了两个接口,而两个接口中定义了同名同参数的默认方法。
// 则实现类在没有重写此两个接口默认方法的情况下,会报错。 ---->接口冲突
//要求:此时实现类必须要重写接口中定义的同名同参数的方法。
s1.method3();
//知识点4:子类(或实现类)继承了父类并实现了接口。父类和接口中声明了同名同参数的方法。(其中,接口中的方法是默认方法)。
//默认情况下,子类(或实现类)在没有重写此方法的情况下,调用的是父类中的方法。--->类优先原则
s1.method4();
}
}
在子类(或实现类)中调用父类或接口中被重写的方法:重写就重写,父类super.
,接口interface.super.
package com.atguigu08._interface.jdk8;
public class SubClass extends SuperClass implements CompareA,CompareB{
@Override
public void method2() {
System.out.println("SubClass:上海");
}
public void method3(){
System.out.println("SubClass:广州");
}
public void method4(){
System.out.println("SubClass:深圳");
}
public void method(){
//知识点5:如何在子类(或实现类)中调用父类或接口中被重写的方法
method4();//调用自己类中的方法
super.method4(); //调用父类中的
method3();//调用自己类中的方法
CompareA.super.method3(); //调用接口CompareA中的默认方法
CompareB.super.method3(); //调用接口CompareB中的默认方法
}
}
4.接口与抽象类
我们再次强调抽象类是对事物的抽象,而接口是对行为的抽象
▶ \blacktriangleright ▶ 抽象类和接口的区别
- 成员区别
∙ \bullet ∙ 抽象类:变量,常量;有构造方法;有抽象方法也有非抽象方法
∙ \bullet ∙ 接口:常量;抽象方法- 关系区别
∙ \bullet ∙ 类与类:继承,单继承
∙ \bullet ∙ 类与接口实现:可以单实现,也可以多实现
∙ \bullet ∙ 接口与接口:继承,单继承,多继承- 设计理念区别
∙ \bullet ∙ 抽象类:对类抽象,包括属性、行为
∙ \bullet ∙ 接口:对行为抽象,主要是行为
▶ \blacktriangleright ▶ 设计步骤
分析:从具体到抽象
实现:从抽象到具体
使用:使用的是具体的类的对象
6.2 接口示例
6.2.1 接口与回调
▶ \blacktriangleright ▶ 回调( callback)
- 定义:
是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。
例:可以指出在按下鼠标或选择某个菜单项时应该采取什么行动。
public class TimerTest {
public static void main(String[] args) {
//TimePrinter实现了 ActionListener接口
//创建该接口的 实现类对象
ActionListener listener = new TimePrinter();
//Timer类:可以使用它在到达给定的时间间隔时发出通告。
// Timer(int interval , ActionListener listener):构造一个定时器, 每隔 interval 毫秒通告 listener—次,
// 第一个参数是发出通告的时间间隔, 它的单位是毫秒。这里希望每隔 10秒钟通告一次。
// 第二个参数是监听器对象。
Timer t = new Timer(10000, listener);//将某个类的对象传递给定时器,然后,定时器调用这个对象的方法。
t.start();
//显示一个包含一条消息和 OK 按钮的对话框。这个对话框将位于其 parent 组件的中央。如果 parent 为 mill , 对话框将显示在屏幕的中央,,
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is " + new Date());
//static Toolkit getDefaultToolkit():获得默认的工具箱。 工具箱包含有关 GUI 环境的信息。
//void beep():发出一声铃响。
Toolkit.getDefaultToolkit().beep();
}
}
6.2.3 对象克隆
▶ \blacktriangleright ▶ Cloneable 接口
- 定义:
∙ \bullet ∙ 这个接口指示一个类提供了一个安全的 clone 方法。
∙ \bullet ∙ clone 方法是 Object 的一个 protected 方法。
∙ \bullet ∙ Cloneable 接口是 Java 提供的一组标记接口 ( tagging interface ) 之一。(有些程序员称之为记号接口 ( maHcer interface)
∙ \bullet ∙ 标记接口不包含任何方法; 它唯一的作用就是允许在类型查询中使用 instanceof:
————————————————————————
第 7 章 异常、 断言和日志
7.1 异常
Java中把不同的异常用不同的类表示,一旦发生某种异常,就创建该异常类型的对象
,并且抛出(throw)。然后程序员可以捕获(catch)到这个异常对象,并处理;如果没有捕获(catch)这个异常对象,那么这个异常对象将会导致程序终止。
▶ \blacktriangleright ▶ Throwable类
- 成员方法
∙ \bullet ∙ public String getMessage():返回此throwable的详细消息字符串
∙ \bullet ∙ public String toString():返回此可抛出的简短描述
∙ \bullet ∙ public void printStackTrace():把异常的错误信息输出在控制台
▶ \blacktriangleright ▶ 异常分类
- 编译时期异常(即checked异常、受检异常):
在代码编译阶段,编译器就能明确警示当前代码可能发生(不是一定发生)
xx异常,并明确督促程序员提前编写处理它的代码。 如果程序员没有编写
对应的异常处理代码,则编译器就会直接判定编译失败,从而不能生成字节码文件。通常,这类异常的发生不是由程序员的代码引起的,或者不是靠加简单判断就可以避免的,例如:FileNotFoundException(文件找不到异常)。
- 运行时期异常(即runtime异常、unchecked异常、非受检异常):
在代码编译阶段,编译器完全不做任何检查,无论该异常是否会发生,编译器都不给出任何提示。只有等代码运行起来并确实发生了xx异常,它才能被发现。通常,这类异常是由程序员的代码编写不当引起的,只要稍加判断,或者细心检查就可以避免。
- 【面试题】说说你在开发中常见的异常都有哪些?
- Error和Exception的区别
7.2 异常处理:编译异常
▶ \blacktriangleright ▶ 异常处理:编译异常
- 异常处理规范:
∙ \bullet ∙一个方法必须声明所有可能抛出的受查异常, 而非受查异常要么不可控制( Error),要么就应该避免发生( RuntimeException)。
即:运行时异常可以不处理,出现问题后,回来修改代码。
∙ \bullet ∙ 如果方法没有声明所有可能发生的受查异常, 编译器就会发出一个错误消息。- 异常处理:捕获处理、向上抛出
∙ \bullet ∙ 仔细阅读一下 Java API 文档, 以便知道每个方法可能会抛出哪种异常, 然后再决定是自己处理,还是添加到 throws 列表中。
∙ \bullet ∙ 应该捕获那些知道如何处理的异常, 而将那些不知道怎样处理的异常继续进行传递抛出。
1.捕获处理:tr..catch..finally....
try{
...... //可能产生异常的代码
}
catch( 异常类型1 e ){
...... //当产生异常类型1型异常时的处置措施
}
catch( 异常类型2 e ){
...... //当产生异常类型2型异常时的处置措施
}
finally{
...... //无论是否发生异常,都无条件执行的语句
}
▶ \blacktriangleright ▶ try….catch.…finally…:
- try:“抛”
程序在执行的过程当中,一旦出现异常,就会在出现异常的代码处,生成对应异常类的对象,并将此对象抛出。一旦抛出,此程序就不执行其后的代码了。- catch:“抓”
∙ \bullet ∙ 针对于抛出的异常对象,进行捕获处理。此捕获处理的过程,就称为抓。
∙ \bullet ∙ 如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息, 其中包括异常的类型和堆栈的内容。
注意:
- try中出现异常后,异常后的部分不执行
- catch :如果多个异常类型满足子父类的关系,则必须将子类声明在父类结构的上面。否则,报错。
- 资源关闭:外面声明对象,try中抛异常对象,catch捕获处理,finally必执行
▶ \blacktriangleright ▶
finally
- 必执行:
不论在try代码块中是否发生了异常事件,catch语句是否执行,catch语句是否有异常,catch语句中是否有return,finally块中的语句都会被执行。- finally与return、
临死遗言,是return我就听,不是就当没听见
2. throws
处理异常:不处理、向上抛、不一定
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{
}
▶ \blacktriangleright ▶ throws 异常处理的一种方式
- 为什么抛出异常:不处理
如果在编写方法体的代码时,某句代码可能发生某个编译时异常,不处理编译不通过,但是在当前方法体中可能不适合处理
或无法给出合理的处理方式
。- 怎么样抛:向上抛
用在方法声明后面,跟的是异常类名,显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。
- 结果: 表示出现异常的一种可能性,
并不一定会发生这些异常
要求
- 继承:父抛子抛
如果超类方法没有抛出任何受查异常,子类也不能抛出任何受查异常。
即:这个子类方法就必须捕获方法代码中出现的每一个受查异常。- 重写:父抛子随
子类重写的方法抛出的异常类型可以与父类被重写的方法抛出的异常类型相同,或是父类被重写的方法抛出的异常类型的子类。
3.throw
产生异常:new异常、throw异常、必异常
throw new 异常类名(参数);
▶ \blacktriangleright ▶ throw:手动生成异常对象的关键字
- 为什么抛出异常:有异常
在实际开发中,如果出现不满足具体场景的代码问题,我们就有必要手动抛出一个指定类型的异常对象。- 怎么样抛:new一个对应异常类的实例对象 并抛出
- 结果:
表示抛出异常对象,由方法体内的语句处理,执行throw一定抛出了某种异常
- throw语句抛出的异常对象,和JVM自动创建和抛出的异常对象一样。
可以抛出的异常必须是Throwable或其子类的实例- 为什么需要手动抛出异常对象?
在实际开发中,如果出现不满足具体场景的代码问题,我们就有必要手动抛出一个指定类型的异常对象。
∙ \bullet ∙ 如果是编译时异常类型的对象,同样需要使用throws或者try…catch处理,否则编译不通过。
∙ \bullet ∙ 如果是运行时异常类型的对象,编译器不提示。
▶ \blacktriangleright ▶ 两种异常处理方式的使用
- 资源关闭:
如果程序代码中,涉及到资源的调用(流、数据库连接、网络连接等),则必须考虑使用try-catch-finally来处理,保证不出现内存泄漏。- 方法重写:
如果父类被重写的方法没有throws异常类型,则子类重写的方法中如果出现异常,只能考虑使用try-catch-finally进行处理,不能throws。
递进:- 递进关系:开发中,方法a中依次调用了方法b,c,d等方法,方法b,c,d之间是递进关系。此时,如果方法b,c,d中有异常,我们通常选择使用throws,而方法a中通常选择使用try-catch-finally。
4.自定义异常:见名知意
▶ \blacktriangleright ▶ 问题1:构造器
- 当我们没有显式的声明类中的构造器时,系统会默认提供一个无参的构造器并且该构造器的修饰符默认与类的修饰符相同
- 当我们显式的定义类的构造器以后,系统就不再提供默认的无参的构造器了。
此时父类没有无参构造器,子类不能使用super()调用父类无参构造器,报错
默认调用父类中空参的构造器。若父类中没有空参的构造器,则编译出错
。 ★ \bigstar ★
第 8 章 泛型程序设计(Generic programming)
8.1 泛型概述
▶ \blacktriangleright ▶ 泛型
- 泛型定义:
∙ \bullet ∙ 是JDK5中引入的特性,它提供了编译时类型安全检测机制,该机制允许在编译时检测到非法的类型
∙ \bullet ∙ 它的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数
∙ \bullet ∙ 顾名思义,就是将类型由原来的具体的类型 参数化,然后在使用\调用时传入具体的类型
即:
在定义类、接口时通过一个标识
(类型参数),表示类中某个属性的类型
或者是某个方法的返回值或参数的类型
。在使用时(继承或实现这个接口、创建对象或调用方法时)传入实际具体的类型参数,也称为类型实参。- 应用:
泛型类、泛型方法、泛型接口- 泛型定义格式:
∙ \bullet ∙ 形参:<类型1,类型>;:指定多种类型的格式,多种类型之间用逗号隔开。这里的类型可以看成是形参
∙ \bullet ∙ 实参:将来具体调用时候给定的类型可以看成是实参,并且实参的类型只能是引用数据类型- 泛型的好处:
∙ \bullet ∙ 把运行时期的问题提前到了编译期间
∙ \bullet ∙ 避免了强制类型转换
▶ \blacktriangleright ▶ 集合使用
集合框架在声明接口和其实现类时,使用了泛型(jdk5.0),在实例化集合对象时
- 如果没有使用泛型,则认为操作的是Object类型的数据。
- 如果使用了泛型,则需要指明泛型的具体类型
一旦指明了泛型的具体类型,则在集合的相关的方法中,凡是使用类的泛型的位置,都替换为具体的泛型类型。
8.2 定义泛型
//泛型类
public class Generic<T> {
public void show(T t){
}
}
//泛型方法
class Generic {
public <T> void show(T t){
}
}
8.4 类型变量的限定
▶ \blacktriangleright ▶ 类型通配符与类型变量 的限定
- 类型通配符定义:
为了表示各种泛型List的父类,可以使用类型通配符- 类型通配符:<?>
∙ \bullet ∙ List<?>:表示元素类型末知的List,它的元素可以匹配任何的类型
∙ \bullet ∙ 这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素添加到其中;- 类型变量
∙ \bullet ∙ 使用大写形式,且比较短。
∙ \bullet ∙ 在 Java 库中, 使用变量 E 表示集合的元素类型, K 和 V 分别表示表的关键字与值的类型。T ( 需要时还可以用临近的字母 U 和 S) 表示“ 任意类型”。- 上限:<?extends 类型>
∙ \bullet ∙ List<? extends Number>:它表示的类型是Number或者其子类型;- 下限:<?super 类型>
∙ \bullet ∙ List<? super Number>:它表示的类型是Number或者其父类型。- 一个类型变量或通配符可以有多个限定
例: (T extends Comparable & Serializable, U extends Comparable)- 限定类型用“ &” 分隔,而逗号用来分隔类型变量。
类型通配符与类型变量:
/** 问题:变量smallest类型为T, 这意味着它可以是任何一个类的对象。怎么才能确保T所属的类有compareTo方法呢? 方法:将T限制为实现了Comparable接口 */
class ArrayAIg {
public static <T extends Comparable> T min(T[] a)
{
if (a== null || a.length == 0) return null;
T smallest = a[0];
for (int i = 1; i < a.length; i++)
if (smallest.compareTo(a[i]) > 0) smallest = a[i];
return smallest;
}
}
▶ \blacktriangleright ▶ 可变参数
- 定义:
可变参数又称参数个数可变,用作方法的形参出现,那么方法参数个数就是可变的了- 格式:
修饰符返回值类型方法名(数据类型…变量名){}
例:public static int sum(int…a){}- 可变参数注意事项
∙ \bullet ∙ 这里的变量其实是一个数组
∙ \bullet ∙ 如果一个方法有多个参数,包含可变参数,可变参数要放在最后
▶ \blacktriangleright ▶ 可变参数的使用
- Arrays.工具类中有一个静态方法:
● public staticListasList((T.a):返回由指定数组支持的固定大小的列表
● 返回的集合不能做增删操作,可以做修改操作- List接口中有一个静态方法:
● public staticListof(E.elements):返回包含任意数量元素的不可变列表
● 返回的集合不能做增删改操作- Set接口中有一个静态方法:
∙ \bullet ∙ public staticSetof(E.elements):返回一个包含任意数量元素的不可变集合
∙ \bullet ∙ 在给元素的时候,不能给重复的元素
●返回的集合不能做增删删操作,没有修改的方法
8.5 泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类。
8.5.1 类型擦除
▶ \blacktriangleright ▶ 类型擦除
- 原始类型 定义:
∙ \bullet ∙ 无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型 ( raw type )。- 类的类型擦除
∙ \bullet ∙ 类的名字:就是删去类型参数后的泛型类型名。
∙ \bullet ∙ 类的内部:擦除( erased) 类型变量, 并替换为第一个限定类型(无限定的变量用 Object)。- 为了提高效率,应该将标签(tagging) 接口(即没有方法的接口)放在边界列表的末尾。
类型擦除:
public class Interval<T extends Comparable & Serializable> implements Serializable {
private T lower;
private T upper;
//...
public Interval(T first, T second) {
. . . }
}
//Interval 原始类型
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
public Interval(Comparable first, Comparable second) {
. . .}
}
8.5.2 翻译泛型表达式
▶ \blacktriangleright ▶ 翻译泛型表达式
- 强制类型转换
∙ \bullet ∙ 当程序调用泛型方法时, 编译器插入强制类型转换。
∙ \bullet ∙ 当存取一个泛型域时也要插人强制类型转换。- 编译器处理原理::
∙ \bullet ∙ 调用类型擦除后的原始方法。
∙ \bullet ∙ 将返回的 Object 类型强制转换为 泛型参数 类型。
例:
Pair< Employee> buddies = . .;
Employee buddy = buddies.getFirst();//调用Pair.getFirst ()返回Object 类型值,将返回的Object类型强制转换为Employee类型。
//泛型类:Pair<T>
public class Pair<T> {
private T first;
private T second;
public Pair() {
}
public Pair(T first, T second) {
}
public T getFirst() {
}
public T getSecond() {
}
public void setFirst(T newValue) {
}
public void setSecond(T newValue) {
}
}
//类型擦除
//原始类型 Pair:
public class Pair {
private Object first;
private Object second;
public Pair(Object first, Object second) {
}
public Object getFirst() {
}
public Object getSecond() {
}
public void setFirst(Object newValue) {
}
public void setSecond(Object newValue) {
}
}
8.5.3 翻译泛型方法
▶ \blacktriangleright ▶ 类型擦除——泛型方法
- 方法的擦除带来了两个复杂问题。
∙ \bullet ∙ 为什么类型擦除与多态发生了冲突。
∙ \bullet ∙ 如何保持多态。- 问题解决:
桥方法(bridge method)- 需要记住有关 Java 泛型转换的事实: ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
∙ \bullet ∙ 虚拟机中没有泛型,只有普通的类和方法。
∙ \bullet ∙ 所有的类型参数都用它们的限定类型替换。
∙ \bullet ∙ 桥方法被合成来保持多态。
∙ \bullet ∙ 为保持类型安全性,必要时插人强制类型转换。
class Datelnterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirstO) >= 0)
super.setSecond(second);
}
}
//类型擦除
class Datelnterval extends Pair {
public void setSecond(LocalDate second) {
. . .}
//从 Pair 继承的 setSecond方法
//编译器在 Datelnterval 类中生成一个桥方法(bridge method)
public void setSecond(Object second){
setSecond((Date) second);
}
调用时:
Datelnterval interval = new Datelnterval(. . .);
Pair<Loca1Date> pair = interval; // OK assignment to superclass
pair.setSecond(aDate);
//虚拟机用 pair 引用的对象调用这个方法。这个对象是Datelnterval 类型的,
//因而将会调用 Datelnterval.setSecond(Object)方法。这个方法是合成的桥方法。
//它调用 Datelnterval.setSecond(Date), 这正是我们所期望的操作效果。
8.6 约束与局限性
将阐述使用 Java 泛型时需要考虑的一些限制,大多数限制都是由类型擦
除引起的。
8.6.1 不能用基本类型实例化类型参数
▶ \blacktriangleright ▶ 不能用类型参数代替基本类型。( 8 种基本类型)
原因:擦除之后, Pair 类含有 Object 类型的域, 而 Object 不能存储 double值
8.6.2 运行时类型查询(instanceof ) 只适用于原始类型
▶ \blacktriangleright ▶ 运行时类型查询只适用于原始类型
- 所有的类型查询只产生原始类型。
例:实际上仅仅测试 a 是否是任意类型的一个 Pair。
∙ \bullet ∙ if (a instanceof Pair< String>) // Error
∙ \bullet ∙ if (a instanceof Pair< T>) // Error
∙ \bullet ∙ 强制类型转换:Pair< Sting> p = (Pair< String>) a;
// Warning-can only test that a is a Pair- .getClass 方法总是返回原始类型。
Pair< String> stringPair = . .
Pair<Employee〉employeePair = . .
if (stringPair.getClass() == employeePair.getClass()) // they are equa
其比较的结果是 true, 这是因为两次调用 getClass 都将返回 Pair.class。
8.6.3 不能创建参数化类型的数组
▶ \blacktriangleright ▶ 不能实例化参数化类型的数组
如果需要收集参数化类型对象, 只有一种安全而有效的方法:
使用 ArrayList:ArrayList<Pair< String>>
//实例化参数化类型的数组:
Pair<String>[] table = new Pair<String>[10]; // Error
//这有什么问题呢? 擦除之后, table 的类型是 Pair[] 可以把它转换为 Object[]
Object[] objarray = table;
//数组会记住它的元素类型, 如果试图存储其他类型的元素, 就会抛出一个 ArrayStoreException 异常:
objarray[0] = "Hello"; // Error component type is Pair
8.6.4 Varargs 警告
▶ \blacktriangleright ▶ 两种方法来抑制警告。
- 为包含 addAll 调用的方法增加注解 @SuppressWamings(“unchecked”。)
- 用@SafeVarargs 直接标注addAll 方法:
//addAll()方法:
@SafeVarargs
public static <T> void addAll(Collections coll, T... ts) {
for (t:ts) coll.add(t);
}
//addAll()方法调用:
Col1ection<Pair<String>> table = . . .;
Pair<String> pairl = . . .;
Pair<String> pair2 = . .
addAll(table, pairl, pair2);//会得到一个警告,而不是错误。
8.6.5 不能实例化类型变量
- 不能使用像 new T(…) 、newT[…] 或 T.class 这样的表达式中的类型变量。
原因:类型擦除将 T 改变成 Object, 而且, 本意肯定不希望调用 new Object()- 解决办法:
让调用者提供一个构造器表达式
8.6.6 不能构造泛型数组
8.6.7 泛型类的静态上下文中类型变量无效
8.6.8 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。实际上, 甚至泛型类扩展 Throwable 都是不合法的。
public class Problem<T> extends Exception {
/* . . . */ } // Error can't extend Throwable
//catch 子句中不能使用类型变量。
public static <T extends Throwable〉void doWork(Class<T> t) {
try
{
do work
}
catch (T e) // Error can 't catch type variable
{
Logger,global.info(...) }
}
//不过, 在异常规范中使用类型变量是允许的。以下方法是合法的:
public static <T extends Throwable> void doWork(T t) throws T {
// OK
try {
//do work
} catch (Throwable realCause) {
t.initCause(realCause);
throw t;
}
}
————————————————————————-
第10 章 多线程:如何创建、进程安全(进程同步)
10.1 基本概念
▶ \blacktriangleright ▶ 程序、进程、线程
- 程序(program):
是为完成特定任务、用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。- 进程(process):
∙ \bullet ∙ 定义:是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。一一生命周期
∙ \bullet ∙ 程序是静态的,进程是动态的;
∙ \bullet ∙ 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域- 线程(thread):
∙ \bullet ∙ 线程定义:进程可进一步细化为线程,是一个程序内部的一条执行路径。
∙ \bullet ∙ 若一个进程同一时间并行执行多个线程,就是支持多线程的;
∙ \bullet ∙ 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小;
∙ \bullet ∙ 一个进程中的多个线程共享相同的内存单元/内存地址空间(堆和方法区),它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。- 进程和线程的l区别
进程是⼀个独⽴的运⾏环境,⽽线程是在进程中执⾏的⼀个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(⽐如I/O)
进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。
10.2 线程的创建和使用:继承Thread类、实现Runnable接⼝ ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
▶ \blacktriangleright ▶ 两种创建方式:Thread类和Runnable接⼝
- 继承Thread类
∙ \bullet ∙ 创建一个继承于Thread类的子类
∙ \bullet ∙ 重写Thread类的run()方法:将此线程执行的操作声明在run()方法中
∙ \bullet ∙ 创建Thread类的子类对象
∙ \bullet ∙ 通过该对象调用start()方法
调⽤了start()⽅法后,虚拟机会先为我们创建⼀个线程,然 后等到这个线程第⼀次得到时间⽚时再调⽤线程内的run()⽅法。- 实现Runnable接口(函数式接⼝):
∙ \bullet ∙ 创建一个实现Runnable接口的类
∙ \bullet ∙ 实现类去实现Runnable中的抽象方法:run()
∙ \bullet ∙ 创建该实现类对象
∙ \bullet ∙ 将此对象作为参数传递给Thread类的构造器中,去创建Thread类的对象
∙ \bullet ∙ 通过Thread类的对象调用start()方法- 两种创建方式的对比:开发中优先选择方 实现Runnable接口
∙ \bullet ∙ 实现的方式没有类的单继承的限制
∙ \bullet ∙ 实现的方式更适合来处理多个线程有共享数据的情况???
创建多线程
方式一:继承于Thread类
1.创建一个继承于Thread类的子类
2.重写Thread类的run()方法:将此线程执行的操作声明在run()方法中
3.创建Thread类的子类对象
4.通过该对象调用start()方法
改进:匿名内部类:创建Thread类的匿名子类
方式二:实现Runnable接口
1.创建一个实现Runnable接口的类
2.实现类去实现Runnable中的抽象方法:run()
3.创建该实现类对象
4.将此对象作为参数传递给Thread类的构造器中,去创建Thread类的对象
5.通过Thread类的对象调用start()方法
两种方式的使用选择:
开发中优先选择方式二,实现Runnable接口
原因:1.实现的方式没有类的单继承的限制
2.实现的方式更适合来处理多个线程有共享数据的情况
// 方式一:继承于Thread类
// 1.创建一个继承于Thread类的子类
class MyThread1 extends Thread {
//2.重写Thread类的run()方法:将此线程执行的操作声明在run()方法中
@Override
public void run() {
...}
}
// 方式二:实现Runnable接口
// 1.创建一个实现Runnable接口的类
class MyThread2 implements Runnable {
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
...}
}
public class ThreadTest1 {
//方式一:
@Test
public void test1() {
//3.创建Thread类的子类对象
MyThread1 t1 = new MyThread1();
//4.通过该对象调用start()方法.启动当前线程,调用该线程run()
t1.start();
}
//方式二:
@Test
public void test2() {
//3.创建该实现类对象
MyThread2 t2 = new MyThread2();
//4.将此对象作为参数传递给Thread类的构造器中,去创建Thread类的对象
Thread thread = new Thread(t2);
thread.setName("线程1");
//5.通过Thread类的对象调用start()方法
thread.start();
//再启动一个线程
Thread thread2 = new Thread(t2);
thread2.setName("线程2");
thread2.start();
}
}
1.创建本质为:创建Thread类或其子类对象thread,调用thread类或其子类对象的start()启动线程,调用当前线程的run()
要想实现多线程,必须在主线程中创建新的线程对象。 Java语言使用Thread类及其子类的对象来表示线程
--------------------------------------------------------------
package com.atguigu01.create.exer1;
class EvenNumberPrint extends Thread implements Runnable{
//用于打印偶数
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class OddNumberPrint extends Thread implements Runnable{
//用于打印奇数
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
}
public class PrintNumberTest {
/**---------------------------------------------------------------------------------------- * 方法一:继承Thread类,重写Thread类的run()方法 */
@Test
public void testThread(){
//方式1:继承子类调start()
EvenNumberPrint t1 = new EvenNumberPrint();
OddNumberPrint t2 = new OddNumberPrint();
t1.start(); //启动线程 ,调用当前线程的run()
t2.start();
//方式2:创建Thread类的匿名子类的对象。
Thread thread = new Thread() {
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
};
thread.start();
//创建Thread类的匿名子类的匿名对象。
new Thread(){
public void run() {
for (int i = 1; i <= 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
/**---------------------------------------------------------------------------------------- * 方法二:实现Runnable接口,实现Runnable中的抽象方法run() */
@Test
public void test(){
//方式1:实现作参调start()
EvenNumberPrint p = new EvenNumberPrint(); //1.创建Runnable接口实现类的对象,作为线程构造器的参数,
Thread t1 = new Thread(p); //2.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
t1.start(); //3.Thread类的实例调用start():
OddNumberPrint o = new OddNumberPrint();
Thread t2 = new Thread(o);
t2.start();
//方式2:提供了Runnable接口匿名实现类的匿名对象,作为Thread构造器参数
new Thread(new Runnable(){
public void run(){
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}).start();
new Thread(new Runnable(){
public void run(){
for (int i = 1; i <= 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}).start();
}
}
2.调用当前
线程的run():子类重写就重写,thread构造器看target
继承Thread类:thread类子类的start()启动线程,调用自己的run(),重写就重写,没有重写调用thread类的run()
实现Runnable接口:thread类的start()启动线程,调用自己的run(),看是否提供target给thread构造器,判断target!=null执行target.run(),即:调用Runnable接口实现类的run()
思考题:判断各自调用的是哪个run()?
-----------------------------------------------------------------------
package com.atguigu01.create.exer2;
public class Exer {
public static void main(String[] args) {
A a = new A();
a.start(); //① 启动线程 ② 调用Thread类的run()
B b = new B(a); //提供target给thread构造器
b.start(); //B无重写run(),调用Thread类的run(),判断target!=null执行target.run(),调用Runnable接口实现类的run()
}
}
//创建线程类A
class A extends Thread {
@Override
public void run() {
System.out.println("线程A的run()...");
}
}
//创建线程类B
class B extends Thread {
private A a;
// public B(A a) {//构造器中,直接传入A类对象
// this.a = a;
// }
public B(A a){
super(a);//Thread(a)
}
// @Override
// public void run() {
// System.out.println("线程B的run()...");
a.run();
// }
}
-----------------------------------------------------------------------
package com.atguigu01.create.exer2;
public class Exer_1 {
public static void main(String[] args) {
BB b = new BB();
new Thread(b){
@Override
public void run() {
System.out.println("CC");
}
}.start();
new Thread(){
}.start();
//thread类匿名子类的start()启动线程,调用自己的run(),重写就重写,没有重写调用thread类的run(),看构造器是否提供target,没有target不执行
//既匿名子类不重写,也不传target作参,啥也没有
}
}
class AA extends Thread{
@Override
public void run() {
System.out.println("AA");
}
}
class BB implements Runnable{
@Override
public void run() {
System.out.println("BB");
}
}
10.3 常用方法与线程的生命周期
1.常用方法
▶ \blacktriangleright ▶ 线程的优先级
- Java的调度方法
∙ \bullet ∙ 同优先级线程:组成先进先出队列(先到先服务),使用时间片策略
∙ \bullet ∙ 高优先级:使用优先调度的抢占式策略- 说明
∙ \bullet ∙ 线程创建时继承父线程的优先级
∙ \bullet ∙ 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
熟悉常用的构造器和方法:
1. 线程中的构造器
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法
- public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
2.线程中的常用方法:
> start():①启动线程 ②调用线程的run()
> run():将线程要执行的操作,声明在run()中。
> currentThread():获取当前执行代码对应的线程
> getName(): 获取线程名
> setName(): 设置线程名
> sleep(long millis):静态方法,调用时,可以使得当前线程睡眠指定的毫秒数
> yield():静态方法,一旦执行此方法,就释放CPU的执行权
> join(): 在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻塞状态,继续执行。
> isAlive():判断当前线程是否存活
3. 线程的优先级:
getPriority():获取线程的优先级
setPriority():设置线程的优先级。范围[1,10]
Thread类内部声明的三个常量:
- MAX_PRIORITY(10):最高优先级
- MIN _PRIORITY (1):最低优先级
- NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。
2.线程状态
▶ \blacktriangleright ▶ 线程运行状态分析:
- run()方法或main()方法结束后,线程就进入终止状态;
- 当线程调用了自身的sleep()方法或其他线程的join()方法:
进程让出CPU,然后就会进入阻塞状态(该状态既停止当前线程,但并不释放所占有的资源,即调用sleep ()函数后,线程不会释放它的“锁标志”。)。当sleep()结束或join()结束后,该线程进入可运行状态,继续等待OS分配CPU时间片。- 线程调用了yield()方法:
∙ \bullet ∙ 意思是放弃当前获得的CPU时间片,回到就绪状态,这时与其他进程处于同等竞争状态,OS有可能会接着又让这个进程进入运行状态;
∙ \bullet ∙ 调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间片从而需要转到另一个线程。- 将要调用的资源被synchroniza(同步):
当线程刚进入可运行状态(注意,还没运行),发现将要调用的资源被synchroniza(同步),获取不到锁标记,将会立即进入锁池状态,等待获取锁标记;- suspend() 和 resume()方法:
∙ \bullet ∙ 两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume()被调用,才能使得线程重新进入可执行状态。
∙ \bullet ∙ 典型地,suspend()和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume()使其恢复。- wait()和 notify() 方法:
∙ \bullet ∙ 当线程调用wait()方法后会进入等待队列,与阻塞状态不同(进入这个状态会释放所占有的所有资源,包括锁
)
∙ \bullet ∙ 进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒
∙ \bullet ∙ 由于notify()只是唤醒一个线程,但我们由不能确定具体唤醒的是哪一个线程,也许我们需要唤醒的线程不能够被唤醒,因此在实际使用时,一般都用notifyAll()方法,唤醒有所线程;
∙ \bullet ∙ 线程被唤醒后会进入锁池,等待获取锁标记。
▶ \blacktriangleright ▶ 问题
- 为什么waite()和notify()必须在synchronized函数或synchronizedblock中进行调用?
因为会对对象的“锁标志”进行操作,所以它们必须在synchronized函数或synchronizedblock中进行调用。如果在non-synchronized函数或non-synchronizedblock中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。- wait() 和 notify() 方法与suspend()和 resume() 方法区别?
初看起来wait() 和 notify() 方法与suspend()和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的suspend()及其它所有方法在线程阻塞时都不会释放占用的锁(如果占用了的话),而wait() 和 notify() 这一对方法则相反。准备执行时,需要竞争同一个synchronized对象锁的 线程是先会被阻塞在该对象的“锁池”中,然后在竞争锁吗?
▶ \blacktriangleright ▶ jdk5.0之前:
▶ \blacktriangleright ▶ jdk5.0及之后:Thread类中定义了一个内部类State
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
3.线程的阻塞状态:synchronized同步阻塞
▶ \blacktriangleright ▶ 线程阻塞状态分析:
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。
阻塞的情况分三种:
- 等待阻塞:
运行的线程执行wait()方法,该线程会释放占用的所有资源(CPU和锁资源),JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,- 同步阻塞:
运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。- 其他阻塞:
运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
▶ \blacktriangleright ▶ 同步阻塞分析
- 程序准备执行:
排序
synchronized修饰的方法,在执行的时候,线程会被排序依次执行。 这时,线程会被阻塞在对象的“锁池”中,只有一个线程会被执行。至于哪个线程被执行,需根据不同的虚拟机实现机制不同。- 执行:
获得同步锁
进入synchronized方法块的线程,会立即持有该对象的锁(同步锁\同步监视器),并从“锁池”中移除,进入就绪状态等待OS分配CPU时间片;- 执行完毕:
释放同步锁
∙ \bullet ∙ 执行完之后释放同步锁,该线程进入锁池,然后锁池里的线程再继续竞争。
∙ \bullet ∙ “锁池”中的线程依据一定规则会有一个线程依次执行该synchronized代码块。
▶ \blacktriangleright ▶ “锁池”和“等待池”
- 每个对象都有自己的“锁池”和“等待池”,用来存放线程。
- 等待池:
wait()
进入synchronized代码块的线程,如果执行wait()方法,对对象的“锁标志”进行操作,就会释放该锁资源和CPU资源,该线程进入“等待池”。
- 线程进入锁池的两种情况:
∙ \bullet ∙ 当线程刚进入可运行状态(注意,还没运行),发现将要调用的资源被synchroniza(同步),获取不到锁标记,将会立即进入锁池状态,等待获取锁标记
∙ \bullet ∙ 当其他线程调用notify()会从等待池任意选择一个线程调入锁池,调用notifyAll()会调用所有等待池线程进入锁池;- 锁池中线程等待获取锁:
∙ \bullet ∙ 锁池里的对象可以竞争锁,优先级高的获得锁的能力更强,获得锁的线程可以进入就绪态状态,等待OS分配CPU时间片;
∙ \bullet ∙ 执行完之后释放锁,然后锁池里的线程再继续竞争。
▶ \blacktriangleright ▶ 同步阻塞问题
- 程序准备运行时,竞争同一个锁的线程对象是在锁池里竞争吗?
是的;- 当某线程获得该锁池的锁后进入就绪状态,等待OS分配CPU时间片,当该线程时间片执行完毕后,释放锁,该线程是进入锁池吗?
是的;时间片完后,释放锁资源,进入同步阻塞状态。
10.4 线程同步机制:临界资源、临界区、同步监视器 ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
▶ \blacktriangleright ▶ 线程的同步
- 临界资源:共享数据
∙ \bullet ∙ 一次仅允许一个进程访问的资源。即临界资源互斥访问;
∙ \bullet ∙ 引起不可再现性是因为临界资源没有互斥访问。- 临界区:
人们把在每个进程中访问临界资源的那段代码称为临界区(critical section)
是多段不同的代码
,写在不同方法中,在程序的不同位置
,共同点是访问临界资源,实现同步机制
- 为什么要同步?
当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。- Java中如何实现同步?
∙ \bullet ∙ synchronized:同步代码块、 同步方法
∙ \bullet ∙ Lock:采用它的实现类ReentrantLock来实例化- 实现同步机制结果:
其实就相当于给临界区代码加“锁”,任何线程想要执行临界区代码,都要先获得“锁”,我们称它为同步锁。
- 面试题:synchronized与Lock的异同?
∙ \bullet ∙ 相同:二者都可以解决线程安全问题
∙ \bullet ∙ 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。
Lock需要手动的启动同步(Lock()),同时结束同步也需要手动的实现(unLock())- 面试题:如何解决线程安全问题?有几种方式
- 当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?(鸿*网络)
需要看其他方法是否使用synchronized修饰,同步监视器的this是否是同一个。
只有当使用了synchronized,且this是同一个的情况下,就不能访问了。
1.synchronized
实现同步 ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
▶ \blacktriangleright ▶ 同步代码块、同步方法
- 同步锁对象\同步监视器:俗称同步锁
∙ \bullet ∙ 同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须共用同一个同步监视器
。
∙ \bullet ∙ 哪个线程获取了锁,哪个线程就能执行需要被同步的代码。- 同步代码块
在实现Runnable接口的方式中,同步监视器可以考虑使用:this。
在继承Thread类的方式中,同步监视器要慎用this,可以考虑使用:当前类.class。- 同步方法:同步锁对象只能是默认的
静态方法:当前类的Class对象(类名.class)
非静态方法:this
同步锁对象是否唯一
Java同步机制:synchronized
方式一:同步代码块
synchronized(同步监视器){
//需要同步的代码,即临界区:问临界资源的那段代码
}
方式二:同步方法:
1.如果操作临界区的代码完整声明在一个方法中,可以将此方法声明为synchronized同步的。
2.同步监视器不需要显式声明
说明:
同步监视器(锁):任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
-----------------------------------------------------------------------------------
使用同步代码块解决线程安全问题: 继承Thread类,实现Runnable接口
//同步代码块
//例1:使用同步代码块解决 继承Thread类创建多线程的 线程安全问题。
class Window extends Thread {
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
//不可以使用this,因为有多个继承Thread类的 子类的对象分别调用run()
//即:this代表者多个不同的线程对象
...
}
}
}
}
//例2:使用同步代码块解决 实现Runnable接口创建多线程的 线程安全问题
class Window2 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
//this:唯一的Runnable实现类对象,通过该对象调用run()方法,即:target.run()
//即:this代表唯一的Runnable实现类对象target
...
}
}
}
}
-----------------------------------------------------------------------------------
使用同步方法解决线程安全问题: 继承Thread类,实现Runnable接口
//同步方法
//例3:使用同步方法解决 实现Runnable接口创建多线程的 线程安全问题
class Window3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private synchronized void show(){
//同步监视器:this
//this:唯一的Runnable实现类对象,通过该对象调用run()方法,即:target.run()
//即:this代表唯一的Runnable实现类对象target
...
}
}
//例4:使用同步方法解决 继承Thread类创建多线程的线程安全问题。
class Window4 extends Thread {
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while (true) {
show();
}
}
//public synchronized void show(){ //此时同步监视器:this。此题目中this:w1,w2,w3,仍然是线程不安全的。
private static synchronized void show(){
//需要加static,变成静态方法
//同步监视器:当前类本身,Window4.class
...
}
}
public class ThreadTest2 {
@Test
public void test1() {
//方式一:
Window w1 = new Window();//多个继承Thread类的子类创建多线程。
Window w2 = new Window();
Window w3 = new Window();
w1.start();
w2.start();
w3.start();
}
@Test
public void test2() {
//方式二:
Window2 window2 = new Window2();
Thread thread1 = new Thread(window2);//共用同一个Runnaable实现类的对象创建多线程,即:共用同一个this
Thread thread2 = new Thread(window2);
Thread thread3 = new Thread(window2);
thread1.start();
thread2.start();
thread3.start();
}
...
}
2.Lock
保证多个线程共用同一个Lock的实例
package com.atguigu04.threadsafemore.lock;
import java.util.concurrent.locks.ReentrantLock;
class Window extends Thread{
static int ticket = 100;
//1. 创建Lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将此对象声明为static final
private static final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
//2. 执行lock()方法,锁定对共享资源的调用
lock.lock();
if(ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally{
//3. unlock()的调用,释放对共享数据的锁定
lock.unlock();
}
}
}
}
synchronized与Lock的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域、遇到异常等自动解锁
- Lock只有代码块锁,synchronized有代码块锁和方法锁
Lock是通过两个方法控制需要被同步的代码,更灵活一些。 - Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更体现面向对象。 - (了解)Lock锁可以对读不加锁,对写加锁,synchronized不可以
- (了解)Lock锁可以有多种获取锁的方式,可以从sleep的线程中抢到锁,synchronized不可以
3.单例懒汉式:存在线程安全问题,(需要使用同步机制来处理)
package com.atguigu04.threadsafemore.singleton;
/** * ClassName: BankTest * Description: * 实现线程安全的懒汉式 * @Author 尚硅谷-宋红康 * @Create 8:51 * @Version 1.0 */
public class BankTest {
static Bank b1 = null;
static Bank b2 = null;
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
b1 = Bank.getInstance();
}
};
Thread t2 = new Thread(){
@Override
public void run() {
b2 = Bank.getInstance();
}
};
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(b1);
System.out.println(b2);
System.out.println(b1 == b2);
}
}
class Bank{
private Bank(){
}
private static volatile Bank instance = null;
//实现线程安全的方式1:同步方法
public static synchronized Bank getInstance(){
//同步监视器,默认为Bank.class
if(instance == null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Bank();
}
return instance;
}
//实现线程安全的方式2:同步代码块
public static Bank getInstance(){
synchronized (Bank.class) {
if(instance == null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Bank();
}
}
return instance;
}
//实现线程安全的方式3:相较于方式1和方式2来讲,效率更高。为了避免出现指令重排,需要将instance声明为volatile
public static Bank getInstance(){
if(instance == null) {
//告诉后面的进程不用再进入同步代码块了
synchronized (Bank.class) {
if (instance == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Bank();
}
}
}
return instance;
}
}
方式3:相较于方式1和方式2来讲,效率更高。
10.5 线程通信
▶ \blacktriangleright ▶ 线程的通信
- 通过synchronized实现互斥
使生产者与消费者进程互斥的去访问货架。实现对临界资源的互斥访问- 通过wait()和notifyAll()方法实现同步
即生产者不会无限生产商品(货架满了生产者就wait等待空位,生产者消费后notify释放空位)
消费者不会无限消费商品(空了消费者就wait一下,等生产了通知消费者)。
1.wait()和 notify()
▶ \blacktriangleright ▶ wait()和 notify() 方法:
- wait():线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用
- notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。(如果被wait()的多个线程的优先级相同,则随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
注意点:
- 此三个方法的使用,必须是在同步代码块或同步方法中。
(超纲:Lock需要配合Condition实现线程间的通信)- 此三个方法的调用者,必须是同步监视器。否则,会报IllegalMonitorStateException异常
- 此三个方法声明在Object类中。
package com.atguigu05.communication;
/** * ClassName: PrintNumberTest * Description: * 案例:使用两个线程打印 1-100。线程1, 线程2 交替打印 */
class PrintNumber implements Runnable{
private int number = 1;
Object obj = new Object();
@Override
public void run() {
while(true){
// synchronized (this) {
synchronized (obj) {
obj.notify();
if(number <= 100){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
obj.wait(); //线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class PrintNumberTest {
public static void main(String[] args) {
PrintNumber p = new PrintNumber();
Thread t1 = new Thread(p,"线程1");
Thread t2 = new Thread(p,"线程2");
t1.start();
t2.start();
}
}
2.高频面试题:wait() 和 sleep()的区别? ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
相同点:一旦执行,当前线程都会进入阻塞状态
不同点:
- 声明的位置:
wait():声明在Object类中
sleep():声明在Thread类中,静态的- 使用的场景不同:
wait():只能使用在同步代码块或同步方法中
sleep():可以在任何需要使用的场景- 使用在同步代码块或同步方法中:
wait():一旦执行,会释放同步监视器
sleep():一旦执行,不会释放同步监视器- 结束阻塞的方式:
wait(): 到达指定时间自动结束阻塞 或 通过被notify唤醒,结束阻塞
sleep(): 到达指定时间自动结束阻塞
3.生产者消费者问题
package com.atguigu05.communication;
/** * ClassName: ProducerConsumerTest * Package: com.atguigu05.communication * Description: * 案例2:生产者&消费者 * 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有 * 固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品 * 了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来 * 取走产品。 * * 分析: * 1. 是否是多线程问题? 是,生产者、消费者 * 2. 是否有共享数据?有! 共享数据是:产品 * 3. 是否有线程安全问题? 有!因为有共享数据 * 4. 是否需要处理线程安全问题?是! 如何处理?使用同步机制 * 5. 是否存在线程间的通信? 存在。 * @Author Folie * @Create 2024/1/17 21:47 * @Version 1.0 */
class Clerk{
private int productNum = 0; //产品数量
//增加产品
public synchronized void addProduct(){
if(productNum >= 20){
//等待
try {
wait(); //从等待位置被唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
productNum++;
System.out.println(Thread.currentThread().getName()+"生产了第"+productNum+"个产品");
//唤醒
//notify();
notifyAll(); //万一其他两个都睡了
}
}
//减少商品
public synchronized void minusProduct(){
if(productNum <= 0 ){
//等待
try {
wait(); //从等待位置被唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//从等待位置被唤醒,被唤醒消费者线程重新申请锁
System.out.println(Thread.currentThread().getName()+"消费了第"+productNum+"个产品");
productNum--;
//唤醒
//notify();
notifyAll(); //万一其他两个都睡了
}
}
}
class Productor extends Thread{
//生产者
private Clerk clerk; //生产者将产品交给店员(Clerk)
public Productor(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
while (true){
System.out.println("生产者生产商品");
//生产慢一点,好观察
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.addProduct();
}
}
}
class Customer extends Thread{
private Clerk clerk; //消费者(Customer)从店员处取走产品
public Customer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
while (true){
System.out.println("消费者消费商品");
//消费慢一点,好观察
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.minusProduct();
}
}
}
public class ProducerConsumerTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk); //继承Thread类的子类对象
Customer customer = new Customer(clerk);
Customer customer2 = new Customer(clerk);
productor.setName("生产者");
customer.setName("消费者");
customer2.setName("消费者2");
productor.start();
customer.start();
customer2.start();
}
}
第无数次忘记main方法
10.6 新增线程创建方式
1.创建线程的方式三:实现Callable接口
▶ \blacktriangleright ▶ 创建线程步骤:
- 创建Callable接口的实现类
- 实现call()方法,将此线程需要执行的操作声明在call()中
- 创建Callable接口实现类对象
- 创建FutureTask对象,将Callable接口实现类对象作为参数传递到FutureTask构造器中,
5 .创建Thread对象,将FutureTask对象作为参数传递到Thread构造器中,调用start()- 获取call()方法的返回值:通过FutureTask对象调用get()方法。
▶ \blacktriangleright ▶ 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
- call()方法有返回值
- call()可以抛出异常
3 . Callable支持泛型
2.创建线程的方式四:线程池
/** * @description: 创建线程的方式四:线程池 * 思路: * 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。 * 可以避免频繁创建销毁、实现重复利用。类似生活中的公共通工具。 * * 好处: * 1.提高响应速度(减少了创建新线程的时间) * 2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建) * 3. 便于线程管理 * corePoolSize:核心池的大小 * maximumPoolSize:最大线程数 * keepAliveTime:线程没有任务时最多保持多长时间后会终止 * * @author: Folie * @date: 2022/8/10 17:29 */
class NumberThread implements Runnable {
@Override
public void run() {
//int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
//sum+=i;
}
}
}
}
class NumberThread1 implements Runnable {
@Override
public void run() {
//int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
//sum+=i;
}
}
}
}
class NumberThread2 implements Callable {
@Override
public Object call() {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
sum += i;
}
}
return sum;
}
}
public class ThreadPool {
public static void main(String[] args) {
//1.创建指定线程数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
ThreadPoolExecutor executorService1 = (ThreadPoolExecutor) executorService;
//2.执行指定的线程,需要提供 Runnable接口或者Callable接口的 实现类对象
executorService.execute(new NumberThread());//适合使用于Runnable
executorService.execute(new NumberThread1());
Future submit = executorService.submit(new NumberThread2());//适合适用于Callable
try {
Object o = submit.get();
System.out.println(o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//3.关闭连接池
executorService.shutdown();
}
}
————————————————————————
第11 章 常用类和基础API
11.1 String: 有变就new,左闭右开
//jdk8中的String源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char[] value; //String对象的字符内容是存储在此数组中
/** Cache the hash code for the string */
private int hash; // Default to 0
> final类:String是不可被继承的
> Serializable:可序列化的接口。凡是实现此接口的类的对象就可以通过网络或本地流进行数据的传输。
> Comparable:凡是实现此接口的类,其对象都可以比较大小。
1.String对象属性值的不可变性
▶ \blacktriangleright ▶ 什么不可变?:String类对象的属性值,引用类型变量value
▶ \blacktriangleright ▶ 为什么不可变?:两个原因
- 外部private:
意味着外部类无法直接获取字符数组,而且String没有提供value数组的get和set方法,外部类无法间接获取字符数组。因此字符串的字符数组内容是不可变的- 内部final: final 域,构造对象之后就不允许改变它们的值
final char value[] : 指明此value数组一旦初始化,其地址就不可变。
▶ \blacktriangleright ▶ 那么到底什么变了???:String类的引用变量指向新的String类对象
即:如果需要对字符串进行修改,就会产生新的String类对象,String类的引用变量指向新的String类对象,新的String类对象的字符数组引用变量指向字符串常量池中新的字符串
▶ \blacktriangleright ▶ String的不可变性的理解:如果需要对字符串进行修改,就会产生新的String类对象
- 赋值:
final意味着字符数组的引用不可改变
当对String引用变量重新赋值时,该字符数组的引用不改变,需要重新指定一个字符串常量的位置进行赋值,不能在原有的位置修改
- 当对现有的字符串进行拼接操作时,需要重新开辟空间保存拼接以后的字符串,不能在原有的位置修改
- 当调用字符串的replace()替换现有的某个字符时,需要重新开辟空间保存修改以后的字符串,不能在原有的位置修改
▶ \blacktriangleright ▶ 两种实例化方式:new String() \ 字符串常量池
- 自变量方式:字符串常量池
- new方式:堆
- 字符串常量都存储在字符串常量池(StringTable)中
- 字符串常量池不允许存放两个相同的字符串常量。
- 字符串常量池,在不同的jdk版本中,存放位置不同。
jdk7之前:字符串常量池存放在方法区
jdk7及之后:字符串常量池存放在堆空间。
2.String对象的连接
▶ \blacktriangleright ▶ String的连接+操作:
有变就new
- 常量 + 常量:
结果仍然存储在字符串常量池中,返回此字面量的地址。注:此时的常量可能是字面量,也可能是final修饰的String类常量
- 常量 + String类变量 或 变量 + 变量 :
都会通过new的方式创建一个新的字符串,返回堆空间中此字符串对象的地址- 调用字符串的intern():
返回的是字符串常量池中字面量的地址- concat(xxx):
不管是常量调用此方法,还是变量调用,同样不管参数是常量还是变量,总之,调用完concat()方法都返回一个新new的对象。
/* * 测试String的连接符:+ * 有变量参与时:调用了StringBuilder的toString()---> new String():返回new String() * 即:产生新对象 * */
@Test
public void test3(){
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
String s4 = "hello" + "world";
String s5 = s1 + "world"; //通过查看字节码文件发现调用了StringBuilder的toString()---> new String():返回new String()
String s6 = "hello" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//false
System.out.println(s3 == s7);//false
System.out.println(s5 == s6);//false
System.out.println(s5 == s7);//false
System.out.println();
String s8 = s5.intern(); //intern():返回的是字符串常量池中字面量的地址
System.out.println(s3 == s8);//true
}
▶ \blacktriangleright ▶ 方法调用
- 特别案例String:
∙ \bullet ∙ 按值调用:方法调用不改变实参的引用地址值
∙ \bullet ∙ String不可变性:且形参也不能通过引用地址修改该引用对象的属性值
- 因为String是不可变的,如果需要对字符串进行修改,就会产生新的String类对象,形参指向新的String类对象
3.String方法 3c e 3i rs
//concat compareTo contains equals isEmpty intern indexOf replace substring valueOf
有变就new,左闭右开
new的方式返回:concat replace substring
package com.atguigu01.string;
public class StringMethodTest {
String构造器的使用
public String() :初始化新创建的 String对象,以使其表示空字符序列。
public String(String original): 初始化一个新创建的 `String` 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。
public String(char[] value) :通过当前参数中的字符数组来构造新的String。
public String(char[] value,int offset, int count) :通过字符数组的一部分来构造新的String。
public String(byte[] bytes) :通过使用平台的**默认字符集**解码当前参数中的字节数组来构造新的String。
public String(byte[] bytes,String charsetName) :通过使用指定的字符集解码当前参数中的字节数组来构造新的String。
@Test
public void test1(){
String s1 = new String();
String s2 = new String("");
String s3 = new String(new char[]{
'a','b','c'});
System.out.println(s3);
}
/* * String与常见的其它结构之间的转换 * * 1. String与基本数据类型、包装类之间的转换(复习) * * 2. String与char[]之间的转换 * * 3. String与byte[]之间的转换(难度) * */
@Test
public void test2(){
int num = 10;
//基本数据类型 ---> String
//方式1:
String s1 = num + "";
//方式2:
String s2 = String.valueOf(num);
//String --> 基本数据类型:调用包装类的parseXxx(String str)
String s3 = "123";
int i1 = Integer.parseInt(s3);
}
//String与char[]之间的转换
@Test
public void test3(){
String str = "hello";
//String -->char[]:调用String的toCharArray()
char[] arr = str.toCharArray();
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
//char[] ---> String:调用String的构造器
String str1 = new String(arr);
System.out.println(str1);//hello
}
//String与byte[]之间的转换(难度)
/* * 在utf-8字符集中,一个汉字占用3个字节,一个字母占用1个字节。 * 在gbk字符集中,一个汉字占用2个字节,一个字母占用1个字节。 * * utf-8或gbk都向下兼容了ascii码。 * * 编码与解码: * 编码:String ---> 字节或字节数组 * 解码:字节或字节数组 ----> String * 要求:解码时使用的字符集必须与编码时使用的字符集一致!不一致,就会乱码。 * * */
@Test
public void test4() throws UnsupportedEncodingException {
String str = new String("abc中国");
//String -->byte[]:调用String的getBytes()
byte[] arr = str.getBytes(); //使用默认的字符集:utf-8
for (int i = 0;i < arr.length;i++){
System.out.println(arr[i]);
}
System.out.println();
//getBytes(String charsetName):使用指定的字符集
byte[] arr1 = str.getBytes("gbk");
for (int i = 0; i < arr1.length; i++) {
System.out.println(arr1[i]);
}
//byte[] ---> String:
String str1 = new String(arr); //使用默认的字符集:utf-8
System.out.println(str1);
String str2 = new String(arr,"utf-8");//显式的指明解码的字符集:utf-8
System.out.println(str2);
//乱码
String str3 = new String(arr1);//使用默认的字符集:utf-8, 但arr使用字符集gbk
System.out.println(str3);
String str4 = new String(arr1,"gbk");//显式的指明解码的字符集:gbk
System.out.println(str4);
}
}
面试题
4.三个类的对比:String、StringBuffer、StringBuilder
1. 三个类的对比:String、StringBuffer、StringBuilder
> String:不可变的字符序列;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
> StringBuffer:可变的字符序列;JDK1.0声明,线程安全的,效率低;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
> StringBuilder:可变的字符序列;JDK5.0声明,线程不安全的,效率高;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
2. StringBuffer/StringBuilder的可变性分析(源码分析):
回顾:
String s1 = new String(); //char[] value = new char[0];
String s2 = new String("abc");// char[] value = new char[]{'a','b','c'};
针对于StringBuilder来说:
内部的属性有:
char[] value; //存储字符序列
int count; //实际存储的字符的个数
StringBuilder sBuffer1 = new StringBuilder();//char[] value = new char[16];
StringBuilder sBuffer1 = new StringBuilder("abc"); //char[] value = new char[16 + "abc".length()];
sBuffer1.append("ac");//value[0] = 'a'; value[1] = 'c';
sBuffer1.append("b");//value[2] = 'b';
...不断的添加,一旦count要超过value.length时,就需要扩容:默认扩容为原有容量的2倍+2。
并将原有value数组中的元素复制到新的数组中。
3. 源码启示:
> 如果开发中需要频繁的针对于字符串进行增、删、改等操作,建议使用StringBuffer或StringBuilder替换String.
因为使用String效率低。
> 如果开发中,不涉及到线程安全问题,建议使用StringBuilder替换StringBuffer。因为使用StringBuilder效率高
> 如果开发中大体确定要操作的字符的个数,建议使用带int capacity参数的构造器。因为可以避免底层多次扩容操作,性能更高。
4. StringBuffer和StringBuilder中的常用方法
增:
append(xx)
删:
delete(int start, int end)
deleteCharAt(int index)
改:
replace(int start, int end, String str)
setCharAt(int index, char c)
查:
charAt(int index)
插:
insert(int index, xx)
长度:
length()
5. 对比三者的执行效率
效率从高到低排列:
StringBuilder > StringBuffer > String
11.2 日期时间
11.3 比较器:实现对象排序
1.Comparable接口 :自然排序
▶ \blacktriangleright ▶ Comparable 接口
- ArrayS.Sort 方法:
static void sort(Object[] a) :按照数字升序排列指定的数组。
例:Arrays.sort(staff);- compareTo 方法
∙ \bullet ∙ 将此对象与指定的对象进行比较以进行排序。
∙ \bullet ∙ 任何实现 Comparable 接口的类都需要包含 compareTo 方法,并且这个方法的参数必须是一个 Object 对象,返回一个整型数值。
∙ \bullet ∙ o1.compareTo(o2):如果o1 > o2,则换位。
即升序排序,最小的在上面- 静态 Double.compare 方法
如果第一个参数小于第二个参数, 它会返回一个负值;如果二者相等则返回 0; 否则返回一个正值。
//在 sort 方法中可能存在下面这样的语句:
if (a[i]. compareTo(a[j]) > 0) {
// rearrange a[i] and a[j]
}
//即:如果a[i].salary > a[j].salary,重新排列
//所以是升序,由小到大排序
//为泛型 Comparable 接口提供一个类型参数
public class Employee implements Comparable<Employee> {
//如果该员工的工资低于otherObject,则为负值,如果工资相同,则为0,否则为正值
public int compareTo(Employee other) {
return Double.compare(salary, other.salary);
}
}
2.Comparator接口:定制排序
▶ \blacktriangleright ▶ 定制排序
- 没有实现Comparable接口:
当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码(例如:一些第三方的类,你只有.class文件,没有源文件)- 实现了Comparable接口,我不喜欢
如果一个类,实现了Comparable接口,也指定了两个对象的比较大小的规则,但是此时此刻我不想按照它预定义的方法比较大小,但是我又不能随意修改,因为会影响其他地方的使用,怎么办?
▶ \blacktriangleright ▶ Comparator接口:compare(o1, o2)方法
- Arrays.Sort 方法:
∙ \bullet ∙ static void sort(T[] a, Comparator< T> c):根据指定的比较器引发的顺序对指定的对象数组进行排序。
∙ \bullet ∙ 参数:一个数组和一个比较器 ( comparator )作为参数, 比较器是实现了 Comparator 接口的类的实例。
例:Arrays.sort(staff,con);- compare 方法
∙ \bullet ∙ int compare(T o1, T o2):比较其两个参数的顺序。
∙ \bullet ∙ compare(o1, o2):如果o1 > o2,则换位。
即升序排序,最小的在上面- 调用
在 sort 方法中可能存在下面这样的语句:
//通过Arrays.sort(staff,con)传入的比较器调用比较方法
if (con.compare(words[i], words[j]) > 0) {
// rearrange a[i] and a[j]
}
//即:如果words[i].length() > words[j].length(),重新排列
//所以是升序,由短到长排序
---------------------------------------------------------------
public class LengthComparator implements Comparator<Employee> {
@Override
public int compare(Employee o1, Employee o2) {
return o1.getName().length()-o3.getName().length();
}
}
测试代码:
public class EmployeeSortTest
{
public static void main(String[] args)
{
Employee[] staff = new Employee[3];
staff[0] = new Employee("H", 35000);
staff[1] = new Employee("Cracker", 75000);
staff[2] = new Employee("Tony Tester", 38000);
//按薪水排序:由小到大
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
System.out.println("--------------------");
//望按名字长度:由小到大
//static <T> void sort(T[] a, Comparator<? super T> c):根据指定的比较器引发的顺序对指定的对象数组进行排序。
Comparator con = new LengthComparator();
Arrays.sort(staff,con);
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
}
}
————————————————————————
第 12 章 集合:(List、Set)、Map
学习的程度把握:
层次1:针对于具体特点的多个数据,知道选择相应的适合的接口的主要实现类,会实例化,会调用常用的方法。
层次2:区分接口中不同的实现类的区别。
层次3:① 针对于常用的实现类,需要熟悉底层的源码 ② 熟悉常见的数据结构 (第14章讲)
12.1 集合框架
1.Collection接口与实现类
▶ \blacktriangleright ▶ 集合主要是两组(单列集合,双列集合)
- 单列集合
Collection接口有两个重要的子接口List 、Set,他们的实现子类都是单列集合
- 双列集合
Map接口的实现子类是双列集合,存放的K-V
▶ \blacktriangleright ▶ 将集合的接口与实现分离
- 定义:
Java 集合类库也将接口( interface) 与 实 现(implementation) 分离。- 使用接口类型存放该接口的实现类(某种集合) 的引用
利用这种方式,一旦改变了想法, 可以轻松地使用另外一种不同的实现。
例:Queue< Customer> expresslane = new CircularArrayQueue<>(100):
Queue< Custoaer> expressLane = new LinkedListQueueoO ;
2.Collection 接口方法 ★ \bigstar ★
▶ \blacktriangleright ▶ 1. 常用方法
- 向Collection中添加元素的要求:
∙ \bullet ∙ 要求元素所属的类一定要重写equals(),比较内容!,否则默认调用Object的equals(),比较地址值
∙ \bullet ∙ 原因:
因为Collection中的相关方法(比如:contains() / remove())在使用时,要调用元素所在类的equals()比较对象内容。
abcdefg hijklmn opqrst uvwxyz
1. 常用方法:(Collection中定义了15个抽象方法。这些方法需要大家熟悉!)
add(Object obj) //注意:add和addAll的区别
addAll(Collection coll)
clear()
contains(Object obj):判断当前集合中是否存在一个与obj对象equals返回true的元素
containsAll(Collection coll):判断coll集合中的元素是否在当前集合中都存在。即coll集合是否是当前集合的“子集”
equals(Object obj):判断当前集合与obj是否相等
hashCode()
isEmpty():判断当前集合是否为空集合
iterator():返回迭代器对象,用于集合遍历
retainAll(Collection coll):从当前集合中删除两个集合中不同的元素,使得当前集合仅保留与coll集合中的元素相同的元素,即交集,即this = this ∩ coll;
remove(Object obj):从当前集合中删除第一个找到的与obj对象equals返回true的元素。
removeAll(Collection coll):从当前集合中删除所有与coll集合中相同的元素。即差集,this = this - this ∩ coll
size()
toArray():返回包含当前集合中所有元素的数组
toArray(T[] a)
▶ \blacktriangleright ▶ 2. 集合与数组的相互转换:
- 集合 —> 数组:toArray()
- 数组 —> 集合:Arrays.asList(Object … objs)
参数必须是引用数据类型数组,即数组元素都是引用数据类型,若是基本数据类型数组,则将该数组作为一个元素加入集合
- 使用Arrays.asList(Object[ ] objs)给List撑场子size
3.迭代器 与 for each ★ \bigstar ★
▶ \blacktriangleright ▶ 迭代器
- Iterator 接口包含 4 个方法
∙ \bullet ∙ default void forEachRemaining(Consumer<? super E> action) :对每个剩余元素执行给定的操作,直到所有元素都被处理或动作引发异常。
∙ \bullet ∙ boolean hasNext():如果迭代具有更多元素,则返回 true 。
∙ \bullet ∙ E next():返回迭代中的下一个元素。
∙ \bullet ∙ default void remove():删除此迭代器返回的最后一个元素(可选操作)。- 通过迭代器实现循环遍历操作
- forEachRemaining()方法:
在 Java SE 8中,甚至不用写循环。可以调用 forEachRemaining 方法并提供一 lambda表达式(它会处理一个元素)。 将对迭代器的每一个元素调用这个 lambda 表达式,直到再没有元素为止。
例:iterator.forEachRemaining(element -> do something with element);- next 方法和 remove 方法的调用具有互相依赖性。
如果调用 remove 之前没有调用 next 将是不合法的。如果这样做, 将会抛出一个 IllegalStateException 异常。
public class CollectionIterator {
public static void main(String[] args) {
Collection col = new ArrayList();
//使用迭代器实现遍历
Iterator iter = col.iterator();
while (iter.hasNext()) {
Object element = iter.next();
//do something with element
}
//使用“ for-each” 实现循环遍历操作
for (Object element : col) {
//do something with element
}
}
}
▶ \blacktriangleright ▶ 增强for循环:
不能引用新对象,可以改变属性值,不可变类属性值也不能改
- 针对于集合来讲,增强for循环的底层仍然使用的是迭代器。
编译器简单地将“ for each” 循环翻译为带有迭代器的循环。- 增强for循环的执行过程中,是将集合或数组中的元素依次赋值给临时引用变量
注意,循环体中对临时引用变量的修改,不能使得集合中的引用类型的元素指向一个新的对象,,将地址值赋值给一个临时引用变量,临时引用变量通过地址可以修改该引用对象的属性值
12.2 List 有序的可重复 索引遍历
▶ \blacktriangleright ▶ List
- 因为List是有序的,进而就有索引,进而就会增加一些针对索引操作的方法。
List:存储有序的、可重复的数据 (“动态”数组)
- ArrayList:
List的主要实现类;线程不安全的、效率高;底层使用Object[]数组存储
在添加数据、查找数据时,效率较高;在插入、删除数据时,效率较低- LinkedList
底层使用双向链表的方式进行存储;在对集合中的数据进行频繁的删除、插入操作时,建议使用此类
在插入、删除数据时,效率较高;在添加数据、查找数据时,效率较低;- Vector
List的古老实现类;线程安全的、效率低;底层使用Object[]数组存储
增
add(Object obj)
addAll(Collection coll)
add(int index, Object ele)
addAll(int index, Collection eles)
删
remove(Object obj)
remove(int index)
改:set(int index, Object obj)
查:get(int index)
长度:size()
遍历:
iterator() :使用迭代器进行遍历
增强for循环
一般的for循环
面试题:remove()默认参数是索引删除,引用参数比较两个对象的内容
相关判断方法(比如:contains() / remove())在使用时,要调用元素所在类重写的equals()比较对象内容
。
1.数组列表 ArrayList: 线程不安全的、效率高;底层使用Object[]数组存储
▶ \blacktriangleright ▶
- ArrayList1.7和1.8的区别(拓*思)
推迟数组的初始化,在使用时才初始化- 数组和 ArrayList 的区别(阿*、*科软)
ArrayList看做是对数组的常见操作的封装。
▶ \blacktriangleright ▶ ArrayList 类
- 为什么要用 ArrayList 取代 Vector 呢?
原因很简单: 如果由一个线程访问 Vector, 代码要在同步操作上耗费大量的时间。而 ArrayList 方法不是同步的。- 因此,建议在不需要同步时使用 ArrayList,,而不要使用 Vector。
- 源码分析
∙ \bullet ∙ 当创建ArrayList)对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。
∙ \bullet ∙ 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍
▶ \blacktriangleright ▶ Vector和ArrayList的比较
▶ \blacktriangleright ▶ ArrayList、LinkedList的区别
ArrayList在添加数据、查找数据时,效率较高;在插入、删除数据时,效率较低
▶ \blacktriangleright ▶ 并发修改异常
- 异常:
ConcurrentModificationException- 产生原因
迭代器遍历的过程中,通过集合对象修改了集合中元素的长度,造成了迭代器获取元素中判断预期修改值和实际修改值不一致- 解决方案
用for循环遍历,然后用集合对象做对应的操作即可
2.链表 LinkedList:双向链表
▶ \blacktriangleright ▶ 链表( linked list)
- 双链表:
在 Java 程序设计语言中,所有链表实际上都是双向链接的(doubly linked)
即:每个结点还存放着指向前驱结点的引用
- 删除:
链表中间删除一个元素是一个很轻松的操作, 即需要更新被删除元素附近的链接。
▶ \blacktriangleright ▶ LinkedList类
- ★ \bigstar ★ listlterator()方法:
∙ \bullet ∙ 返回一个实现了 Listlterator 接口的迭代器对象。
例:返回头迭代器
ListIterator< String> headIter = list.listIterator();- ★ \bigstar ★ListIterator< E> listIterator(int index)
从列表中的指定位置开始,返回此列表中元素的列表迭代器(按适当的顺序)。
例:返回尾迭代器
ListIterator< String> rearIter = list.listIterator(list.size());- 为什么Linkedlist类没有实现Listiterator接口,却可以使用listIterator()方法获得迭代器?
因为listIterator()方法是Linkedlist类的成员方法,返回此列表中元素的双向迭代器。
▶ \blacktriangleright ▶ 链表迭代器-Listlterator接口
- 反向遍历链表:
∙ \bullet ∙ 获得尾迭代器
∙ \bullet ∙ 判断:boolean hasPrevious()
∙ \bullet ∙ 返回遍历对象:E previous():向链表前部移动,返回越过的对象。与 next() 方法一样。- 为了避免发生并发修改的异常,遵循下述简单规则:
∙ \bullet ∙ 可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。
∙ \bullet ∙ 另外,再单独附加一个既能读又能写的迭代器。
- add()方法
∙ \bullet ∙Listlterator接口特有的方法
∙ \bullet ∙ 在迭代器位置之前添加一个新对象。
∙ \bullet ∙ 总结:
add 方法依赖于迭代器的位置。- remove()方法:
∙ \bullet ∙ 调用 next 之后,remove 方法确实删除了迭代器左侧的元素。
∙ \bullet ∙ 调用 previous 就会将右侧的元素删除掉;
∙ \bullet ∙ 并且不能连续调用两次remove()。
∙ \bullet ∙ 总结:
remove() 方法依赖于迭代器的状态(迭代器向前还是向后)。
- set() 方法
用一个新元素取代调用 next 或 previous 方法返回的上一个元素。- nextlndex()方法 与previouslndex() 方法
∙ \bullet ∙ nextlndex 方法:返回下一次调用 next 方法时返回元素的整数索引;
∙ \bullet ∙ previouslndex 方法返回下一次调用 previous 方法时返回元素的整数索引。
链表
public class LinkedListTest {
public static void main(String[] args) {
List<String> a = new LinkedList<>();
...添加...
//Iterator与Listlterator区别:
//Iterator接口中没有add方法。其子接口Listlterator, 其中包含add方法:
ListIterator<String> aIter = a.listIterator();
Iterator<String> bIter = b.iterator();
//插入:将b中单词加入a中
//在某位置插入
//头插法
System.out.println(">>>头插:");
while (bIter.hasNext()) {
aIter.add(bIter.next());
}
//尾插法
System.out.println(">>>尾插:");
while (aIter.hasNext()) {
aIter.next();
//直到a的迭代器越过链表的最后一个元素时开始添加元素(即 hasNext 返回 false时)
if (!aIter.hasNext()){
while (bIter.hasNext()){
aIter.add(bIter.next());
}
}
}
System.out.println("-------------");
//删除
//获得头指针
ListIterator<String> headIter = a.listIterator();
//删除迭代器左侧
headIter.next();
headIter.remove();
//获得尾指针
ListIterator<String> rearIter = a.listIterator(a.size());
//删除迭代器右侧
rearIter.previous();
rearIter.remove();
}
}
12.2 Set :存储无序不重复,底层实现是Map,迭代器遍历
1.HashSet去重机制: hashCode()+equals(),数组链表红黑树
▶ \blacktriangleright ▶ HashSet 类
- 实现了基于散列表的集合:
底层使用的是HashMap,即使用数组+单向链表+红黑树结构进行存储。(jdk8中)- 用来过滤重复数据
- 无序性:
根据添加的元素的哈希值,计算的其在数组中的存储位置。此位置不是依次排列的,表现为存储位置无序性
。
只有不关心集合中元素的顺序时才应该使用 HashSet。
- 不可重复性:添加到Set中的元素是不能相同的。
比较的标准,需要判断hashCode()得到的哈希值
以及equals()得到的boolean型的结果。
哈希值相同且equals()返回true,则认为元素是相同的。
▶ \blacktriangleright ▶ HashSet集合添加一个元素的过程:
- HashSet集合存储元素:
要保证元素唯一性,需要重写hashCode)和equals()
- 示例
2.TreeSet的去重机制:Comparable \ Comparator ,有序遍历可比较;存储结构红黑树
红黑树:进行中序遍历,可以得到一个递增的有序序列
▶ \blacktriangleright ▶ TreeSet
- TreeSet为什么是有序集合?
树集是一个有序集合( sorted collection) :底层使用红黑树存储,进行中序遍历,可以得到一个递增的有序序列- 如何构造TreeSet?
∙ \bullet ∙ 即:红黑树的建立、红黑树的插入、TreeSet数据添加的过程,可以按照添加的对象的指定的属性的大小进行添加
∙ \bullet ∙ 指定的属性的大小:Comparable \ Comparator- 得到的TreeSet有什么用?
已经有序
在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。
即:可以按照添加的元素的指定的属性的大小顺序进行遍历。
- 要求添加到TreeSet中的元素必须是同一个类型的对象,否则会报ClassCastException.
- 对象必须可比较:两种排序方式 ★ \bigstar ★
∙ \bullet ∙ 自然排序:类必须实现 Comparable 接口,重写conpareTo()方法 。
∙ \bullet ∙ 定制排序:构造TreeSet时必须提供一个 Comparator 接口实现类对象 。
▶ \blacktriangleright ▶ HashSet和TreeSet分别如何实现去重的? ★ \bigstar ★
- HashSet的去重机制:
hashCode()+equals():底层先通过存入对象,进行运算得到一个hash值,通过hash值得到对应的索引,如果发现table索引所在的位置,
∙ \bullet ∙ 没有数据,就直接存放
∙ \bullet ∙ 如果有数据,就进行equals比较[遍历比较],如果比较后,不相同,就加入,否则就不加入.TreeSet的去重机制:Comparable \ Comparator
∙ \bullet ∙ 自然排序:类必须实现 Comparable 接口,重写compareTo()方法 ,如果方法返回0,就认为是相同的元素/数据,就不添加
∙ \bullet ∙ 定制排序:构造TreeSet时必须提供一个 Comparator接口实现类对象 。重写compare()方法,如果方法返回0,就认为是相同的元素/数据,就不添加
面试题:
HashSet中添加元素的过程:
12.3 Map映射
散列表\哈希表 散列函数 拉链法
▶ \blacktriangleright ▶ 散列表:
- 散列函数 :散列码(hashcode)
散列表为每个对象计算一个整数, 称为散列码- 散列表实现:
在 Java 中,散列表用链表数组红黑树实现。每个列表被称为桶( bucket)
- 散列冲突( hashcollision)
处理冲突:线性探测法、链地址法- 装填因子( load factor)
∙ \bullet ∙ 装载因子是指所有关键子填充哈希表后饱和的程度,
∙ \bullet ∙ 装载因子 = 关键字总数 n / 哈希表的长度 l 装载因子=关键字总数n/哈希表的长度l 装载因子=关键字总数n/哈希表的长度l
∙ \bullet ∙ 决定何时对散列表进行再散列。
例:如果装填因子为 0.75 (默认值) ,而表中超过 75%的位置已经填人
元素, 这个表就会用双倍的桶数自动地进行再散列。- 散列表实现数据结构
∙ \bullet ∙ set 类型。set 是没有重复元素的元素集合。set 的 add方法首先在集中查找要添加的对象,如果不存在,就将这个对象添加进去。
▶ \blacktriangleright ▶ HashMap散列映射 与 TreeMap树映射
- 散列映射对键进行散列
- 树映射用键的整体顺序对元素进行排序, 并将其组织成搜索树。
- 应该选择散列映射还是树映射呢?
与集一样, 散列稍微快一些, 如果不需要按照排列顺序访问键, 就最好选择散列。
- [面试题] 区别HashMap和Hashtable、区别HashMap和LinkedHashMap、HashMap的底层实现 ★ \bigstar ★ ★ \bigstar ★
1.HashMap:null的key-value,数组链表红黑树,添加过程要记住 (1建2索3比较,树化链化与扩容)
▶ \blacktriangleright ▶ 1建2索3比较,树化链化与扩容
创建添加初始化
,大小16扩2倍
- hash1 hash2,indexFor
确定i
。
- i有元素
哈希2
,七上八下头尾插
。哈希2同equals
,false添加true替换
。
hash冲突:equals不等时,hash值可能相等
解决hash冲突:拉链法,红黑树
- 个数低6单链表,
个数
(8)长度
(64)红黑树。
当使用红黑树的索引i位置上的元素的个数低于6的时候,就会将红黑树结构链化。
当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
- 为什么使用红黑树呢?
红黑树进行put()/get()/remove()操作的时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n),比单向链表的时间复杂度O(n)的好。性能更高。
▶ \blacktriangleright ▶ HashMap中元素的特点
- key:键集Set
HashMap中的所有的key彼此之间是不可重复的、无序的。所有的key就构成一个Set集合。—>key所在的类要重写hashCode()和equals()
key是实现了Set 接口或其子接口的对象。- value:值集Collection
HashMap中的所有的value彼此之间是可重复的、无序的。所有的value就构成一个Collection集合。—>value所在的类要重写equals()
value是实现了Collection 接口或其子接口的对象。- entry:键/值对集Set
HashMap中的所有的entry彼此之间是不可重复的、无序的。所有的entry就构成了一个Set集合。
HashMap中的一个key-value,就构成了一个entry。
entry是Map.Entry 接口的实现类的对象
Map的遍历操作
public void test5(){
HashMap map = new HashMap();
map.put("AA",56);
map.put(67,"Tom");
map.put("BB",78);
map.put(new Person("Jerry",12),56);
//遍历key集:Set keySet()
Set keySet = map.keySet();
//使用迭代器
Iterator iterator = keySet.iterator();
while(iterator.hasNext()){
Object key = iterator.next();
System.out.println(key);
}
//遍历value集:Collection values()
//推荐使用增强for
Collection values = map.values();
for(Object obj : values){
System.out.println(obj);
}
//遍历entry集:Set entrySet()
Set entrySet = map.entrySet(); //entry是Map.Entry接口的实现类的对象
//使用迭代器
//接口多态:Map接口的内部接口Entry类型引用变量 引用其实现类类对象
Iterator<Map.Entry> itor = entrySet.iterator();
while(itor.hasNext()){
Map.Entry entry = itor.next();
System.out.println(entry.getKey() + "--->" + entry.getValue());
}
}
/** 映射 1.声明 2.增删改查:put(key,value),remove(key),get(key)/getOrDefault(key,default) 3.遍历:映射视图(条目集合) 3.1 键集合:Set<String> keys = map.keySet(); 3.2 键值对集合:Set<Map.Entry<String, Integer>> set = map.entrySet(); 4.forEach方法 */
public class HashSetTest {
public static void main(String[] args) {
// 创建 HashMap 对象 Sites
Map<String, Integer> map = new HashMap<>();
// 添加键值对
map.put("Google", 1);
map.put("Runoob", 2);
map.put("Taobao", 3);
map.put("Zhihu", 4);
System.out.println(map);
//对键集合操作
Set<String> keys = map.keySet();
for (String key : keys) {
//do something with key
}
//对键值对集合操作,通过枚举条目
Set<Map.Entry<String, Integer>> set = map.entrySet();
for (Map.Entry<String, Integer> entry : set) {
String k = entry.getKey();
Integer v = entry.getValue();
//do something with k, v
}
//forEach 方法
map.forEach((k,v) ->{
//do somethingwith k, v
});
}
}
2.TreeMap:类比TreeSet (key同类可比较,顺序遍历;红黑树)
▶ \blacktriangleright ▶ TreeMap
- 底层使用红黑树存储;
- 可以按照添加的key-value中的key元素的指定的属性的大小顺序进行遍历。
- 需要考虑使用①自然排序 ②定制排序。
- 要求:向TreeMap中添加的key必须是同一个类型的对象。
3.链接散列集LinkedHashSet 与 链接散列映射LinkedHashMap
▶ \blacktriangleright ▶ LinkedHashSet 和LinkedHashMap类
- 定义:
双链表用来记住插入元素项的顺序。 这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中。
- 链接散列映射LinkedHashMap 将用访问顺序, 对映射条目进行迭代。而不是插入顺序
∙ \bullet ∙ 即:每次调用 get 或put, 受到影响的条目将从当前的位置删除, 并放到条目链表的尾部(只有条目在链表中的位置会受影响, 而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。- 访问顺序对于实现高速缓存的“ 最近最少使用” 原则十分重要。
∙ \bullet ∙ 例:可能希望将访问频率高的元素放在内存中, 而访问频率低的元素则从数据库中读取。
∙ \bullet ∙ 当在表中找不到元素项且表又已经满时, 可以将迭代器加入到表中, 并将枚举的前几个元素删除掉。这些是近期最少使用的几个元素。
链接散列映射与高速缓存
public class LinkedHashMapTest {
public static void main(String[] args) {
Map<String, Employee> staff = new LinkedHashMap<>();
staff.put("144-25-5464", new Employee("Amy Lee"));
staff.put("567-24-2546", new Employee("Harry Hacker"));
staff.put("157-62-7935", new Employee("Gary Cooper"));
staff.put("456-62-5527", new Employee("Francesca Cruz"));
//枚举键:staff.keySet().iterator()
Iterator keyIter = staff.keySet().iterator();
//为什么这里不能使用listIterator():因为keySet()返回的是集合set,只有linkedlist类才有listIterator()方法。
while (keyIter.hasNext()){
System.out.println(keyIter.next());
}
System.out.println("---------------");
//枚举值:staff.values().iterator()
Iterator valueiter = staff.values().iterator();
while (valueiter.hasNext()){
System.out.println(valueiter.next());
}
//链接散列映射
//LinkedHashMap<K, V>(initialCapacity, loadFactor, true);
//例如,下面的高速缓存可以存放 100 个元素:
Map<String, Employee> cache = new LinkedHashMap<String, Employee>(128, 0.75F, true) {
//匿名内部类:继承了LinkedHashMap类的匿名子类对象
@Override
protected boolean removeEldestEntry(Map.Entry<String, Employee> eldest) {
return size() > 100;
}
};
}
}
12.3 Collections工具类 :排序反转,最值复制
▶ \blacktriangleright ▶ Collections工具类 ★ \bigstar ★ ★ \bigstar ★ ★ \bigstar ★
- Collections是一个操作Set、List和Map等集合的工具类
- Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作
- 排序操作:(均为static方法) ★ \bigstar ★
∙ \bullet ∙ sort(List):根据元素的自然顺序对指定List集合元素按升序排序
∙ \bullet ∙ sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
∙ \bullet ∙ shuffle(List):对List集合元素进行随机排序
∙ \bullet ∙ reverse(List):反转List中元素的顺序- 交换元素
∙ \bullet ∙ swap(List,int,int):将指定list集合中的i处元素和j处元素进行交换- 查找
∙ \bullet ∙ Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
∙ \bullet ∙ Object max(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素
∙ \bullet ∙ int frequency(Collection,Object):返回指定集合中指定元素的出现次数
∙ \bullet ∙ void copy(List dest,List src):将src中的内容复制到dest中- 替换
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值
12.4 视图与包装器
通过使用视图( views) 可以获得其他的实现了 Collection 接口和 Map 接口的类 的对象。???不懂
12.4.1 轻量级集合包装器
- keySet ()方法
返回一个实现 Set接口的 KeySet类对象, 通过这个KeySet类的方法对原映射进行操作。这种集合称为视图。- entrySet() 方法
返回一个实现 Set接口的EntrySet类对象, 通过这个EntrySet类的方法对原映射进行操作。这种集合称为视图。- 总结:视图是集合的子类
final class KeySet extends AbstractSet<K> {
...}
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
...}
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
12.2.5 队列与双端队列
▶ \blacktriangleright ▶ 双端队列
- 有两个端头的队列, 即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。
▶ \blacktriangleright ▶ Deque 接口
- 实现类: ArrayDeque 和 LinkedList 类
12.2.6 优先级队列
▶ \blacktriangleright ▶ 优先级队列(priority queue)
- 定义:
优先级队列中的元素可以按照任意的顺序插人,却总是按照排序的顺序
进行检索。- 堆(heap)
优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一
个可以自我调整的二叉树,对树执行添加( add) 和删除(remore) 操作, 可以让最小的元素移动到根,而不必花费时间对元素进行排序。- 使用:
与 TreeSet—样,一个优先级队列既可以保存实现了 Comparable 接口的类对象, 也可以保存在构造器中提供的 Comparator 对象。- 与TreeSet 区别:
优先级队列的迭代并不是按照元素的排列顺序访问的。而删除却总是删掉剩余元素中优先级数最小的那个元素。
————————————————————————
第15 章 IO流:流对象,
15.1 File:对应硬盘上的一个文件或目录,不会在硬盘上创建文件或目录
仅仅是内存层面的File类对象,该对象有一个位置属性,读取文件时使用该属性
▶ \blacktriangleright ▶ File概述和构造方法
- 定义:
它是文件和目录路径名的抽象表示- File对象
● 文件和目录是可以通过File封装成对象的
● 对于File而言,其封装的并不是一个真正存在的文件,仅仅是一个路径名而已。它可以是存在的,也可以是不存在的。
∙ \bullet ∙ 将来是要通过具体的操作把这个路径的内容转换为具体存在的- 构造方法
“>
▶ \blacktriangleright ▶ File类方法:增create删delete改renameTo查3get,3判断2遍历
▶ \blacktriangleright ▶ 绝对路径和相对路径的区别
- 绝对路径:
完整的路径名,不需要任何其他信息就可以定位它所表示的文件。例如:E:\itcast\Vava.tbxt- 相对路径:
必须使用取自其他路径名的信息进行解释。例如:myFile\Vava.tbt
▶ \blacktriangleright ▶ 遍历目录
10.2 字节流
▶ \blacktriangleright ▶ IO流概述和分类
- 按照数据的流向
∙ \bullet ∙ 输入流:读数据
∙ \bullet ∙ 输出流:写数据- 按照数据类型来分
∙ \bullet ∙ 字节流
字节输入流;字节输出流
∙ \bullet ∙ 字符流
字符输入流;字符输出流- 一般来说,我们说流的分类是按照数据类型来分的那么这两种流都在什么情况下使用呢?
∙ \bullet ∙ 如果数据通过Window自带的记事本软件打开,我们还可以读懂里面的内容,就使用字符流,
∙ \bullet ∙ 否则使用字节流。如果你不知道该使用哪种类型的流,就使用字节流
▶ \blacktriangleright ▶ 字节流写数据
- 字节流抽象基类
●InputStream:这个抽象类是表示字节输入流的所有类的超类
●OutputStream:这个抽象类是表示字节输出流的所有类的超类
●子类名特点:子类名称都是以其父类名作为子类名的后缀- FileOutputStream:文件输出流用于将数据写入File
●FileOutputStream(String name):创建文件输出流以指定的名称写入文件- 使用字节输出流写数据 的步骤:
∙ \bullet ∙ 创建字节输出流对象(调用系统功能创建了文件,创建字节输出流对象,让字节输出流对象指向文件)
∙ \bullet ∙ 调用字节输出流对象的写数据方法
∙ \bullet ∙ 释放资源(关闭此文件输出流并释放与此流相关联的任何系统资源)- 字节流写数据实现换行
●写完数据后,加换行符
windows:\r\n
linux:\n
mac:\r- 字节流写数据如何实现追加写入呢?
public FileOutputStream(String name,boolean append)
●创建文件输出流以指定的名称写入文件。如果第二个参数为true,则字节将写入文件的末尾而不是开头
public class FileInputStreamDemo02 {
public static void main(String[] args) throws Exception {
//创建字节输入流对象
FileInputStream fis = new FileInputStream("C:\\Users\\86173\\Desktop\\窗里窗外.txt");
//创建字符输出流对象
FileOutputStream fos = new FileOutputStream("idea_demo\\窗里窗外.txt");
//调用字节输入流对象的读 数据方法
//一次读取一个字节数组,一次写入一个字节数组
byte[] bys = new byte[1024];
int by;//保存读取一个字节数组中的字节
while ((by=fis.read(bys))!=-1){
//循环读取一个字节数组中的字节
//调用字节输出流对象的写数据方法
fos.write(bys,0,by);
}
//关闭文件
fis.close();
fos.close();
}
}
▶ \blacktriangleright ▶ 字节缓冲流
- 字节缓冲流:
∙ \bullet ∙ **BufferOutputStream:**该类实现缓冲输出流。通过设置i这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用
∙ \bullet ∙ BufferedInputStream: 创建BufferedInputStream将创健一个内部缓冲区数组。当从流中读取或跳过字节时,内部缓
冲区将根据需要从所包含的输入流中重新填充,一次很多字节
第16章 网络编程
第17章 反射:动态的
获取指定对象所属的类,创建运行时类的对象,调用指定的结构(属性、方法)等。
▶ \blacktriangleright ▶ 反射库( reflection library)
- 定义:
提供了一个非常丰富且精心设计的工具集, 以便编写能够动态操纵 Java 代码的程序。- 在 java.lang.reflect 包中有三个类 Field、 Method 和 Constructor 分别用于描述类的域、 方法和构造器。
动态的
获取指定对象所属的类,创建运行时类的对象
1.Class类:动态的获取指定对象所属的类和类所有的结构
(父类、接口们、包、带泛型的父类、父类的泛型等)
描述类的类,具体的类作为该类的实例
▶ \blacktriangleright ▶ Class类
- 注意:
一个 Class 对象实际上表示的是一个类型,而这个类型未必一定是一种类。
例如:int 不是类, 但 int.class 是一个 Class 类型的对象。- Class类中的 getFields、 getMethods 和 getConstructors 方法将分别返回类提供的public域、方法和构造器数组, 其中包括超类的公有成员。
- Class 类的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、 方法和构造器, 其中包括私有和受保护成员,但不包括超类的成员。
▶ \blacktriangleright ▶ 获取Class类的对象:
运行时类. 对象. Class类.
加载到内存中的.class文件对应的结构即为Class的一个实例,即:一个类的字节码文件对象,运行时类
三种方式获取一个类的字节码文件对象 (Class类型的对象.)
- 使用运行时类的静态class属性来获取该类对应的Classi对象。
例:Student.class
将会返回Student类对应的Classi对象- 调用对象的getClass()方法,返回该对象所属类对应的Class对象
该方法是Object类中的方法,所有的Java对象都可以调用该方法
例:
ArrayList< Integer> array= new ArrayList< Integer>();
Class<? extends ArrayList> c = array.getClass();- Class类中的静态方法: forName(String className)
该方法需要传入字符串参数,该字符串参数的值是某个类的全路径,也就是完整包名的路径。
例:Class<?> c4 = Class.forName(“book.base_05.reflection.Student”);
/* 三种方式获取一个类的字节码文件对象 Class类型的对象. 1.调用运行时类的静态属性:使用类的class属性来获取该类对应的Class对象。 举例:Student.class将会返回Student类对应的Classi对象 2.调用运行时类的对象的getClass()方法,返回该对象所属类对应的Class对象 该方法是Object中的方法,所有的Java对象都可以调用该方法 3.调用Class的静态方法forName(String className),该方法需要传入字符串参数, 该字符串参数的值是某个类的全路径,也就是完整包名的路径 */
public class ReflectionDemo {
public static void main(String[] args) throws ClassNotFoundException {
//1.使用类的class属性来获取该类对应的Classi对象。
Class<Student> c1 = Student.class;
Class<Student> c2 = Student.class;
//2.调用对象的getClass0方法,返回该对象所属类对应的Class对象
Student s = new Student();
Class<? extends Student> c3 = s.getClass();
//3.使用Class类中的静态方法forName(String className),该方法需要传入字符串参数,
//该字符串参数的值是某个类的全路径,也就是完整包名的路径
Class<?> c4 = Class.forName("book.base_05.reflection.Student");
}
}
2.Constructor类:创建运行时类的对象
▶ \blacktriangleright ▶ 获取构造方法并使用
步骤:
- 获得Class类的对象c
- 通过对象c获得Constructor类对象con
- 通过对象con创建Student类的对象并初始化,即:创建和初始化构造函数的声明类 的新实例。
/** * 使用构造器创建对象并初始化对象 * 步骤: * 1.获得Class类的对象c * 3.通过对象c获得Constructor类对象con * 4.通过对象con创建Student类的对象并初始化 */
public class ReflectionDemo02 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//1.获得Class类的对象c,对象c引用的是Student类的字节码文件对象(Class类型的对象)
Class<Student> c = Student.class;
//3.获得Constructor类对象con,对象con引用是指定的某个Constructor对象
//public Student(String name, int age, String address)
//基本数据类型也可以通过.class得到对应的Class类型
Constructor<Student> con = c.getConstructor(String.class,int.class,String.class);
//4.创建Student类的对象
Student stu = con.newInstance("林青霞",30,"西安");
System.out.println(stu);
}
}
▶ \blacktriangleright ▶ 暴力反射
- 原因:
设置私有的域和调用私有方法时,会出现 IllegalAccessException(非法访问异常)- 解决方法:
访问前先设置访问性,setAccessible()
例:ConstructorName.setAccessible(true);
fieldName.setAccessible(true);
methodName.setAccessible(true);
**
* 暴力反射:使用私有的构造器创建对象
* */
public class ReflectionDemo03 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<?> c = Class.forName("book.ch04.reflection.Student");
Constructor<?> con = c.getDeclaredConstructor(String.class);
//一般情况下:不能通过私有构造器创建对象
//暴力反射:
//void setAccessible(boolean flag):将此反射对象的 accessible标志设置为指示的布尔值。
//值true表示反射对象在使用时应禁止检查Java语言访问控制。
con.setAccessible(true);
Object ojb = con.newInstance("林青霞");
System.out.println(ojb);
}
}
调用指定的结构(属性、方法)
3.Field类:属性调对象
▶ \blacktriangleright ▶ 获取成员变量并使用
步骤:
- 获取Class类对象c
- 获取运行时类对象:
使用构造器创建对象obj,即:创建和初始化构造函数的声明类 的新实例。- 获取属性类对象:
通过对象c获得Field类的对象field,即通过类Class对象获得某个具体的 域对象- 使用:通过属性类对象
通过对象field调用set()方法,
例:field.set(obj,value)
获取成员变量并使用:
/** 反射获取成员变量并使用 步骤: 1.获取Class类对象c 2.使用构造器创建对象obj 3.通过对象c获得Field类的对象field,即通过类对象获得某个具体的域对象 4.通过对象field调用set()方法,field.set(obj,value) */
public class ReflectionDemo05 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//1.获取Class类对象c
Class<?> c = Class.forName("book.ch04.reflection.Student");
//2.使用构造器创建对象obj
Constructor<?> con = c.getConstructor();
Object obj = con.newInstance();
//3.通过对象c获得Field类的对象field
//getField()方法只能获取公共的Field类对象
//getDeclaredField()方法才能获取保护的和私有的
//Field[] getFields():返回包含一个数组 Field对象反射由此表示的类或接口的所有可访问的公共字段 类对象。
//Field[] getDeclaredFields():返回的数组 Field对象反映此表示的类或接口声明的所有字段 类对象。
//Field namefields = c.getField("name");//NoSuchFieldException
Field namefields = c.getDeclaredField("name");
//Field agefields = c.getField("age");//NoSuchFieldException
Field agefields = c.getDeclaredField("age");
Field addressfields = c.getField("address");
// 4.通过对象field调用set()方法
//一般情况下:不能直接使用类的私有域和私有方法
//暴力反射:设置访问性,setAccessible()
namefields.setAccessible(true);
namefields.set(obj, "林青霞");//IllegalAccessException
agefields.set(obj, 30);
addressfields.set(obj, "西安");//给obj的成员变量addressfields赋值西安
System.out.println(obj);
}
}
4.Method类:方法调对象
▶ \blacktriangleright ▶ 获取成员方法并使用
- 获取Class类对象c
- 获取运行时类对象:
使用构造器创建对象obj,即:创建和初始化构造函数的声明类 的新实例。- 获取方法类对象
通过对象c获得Method类的对象method
∙ \bullet ∙ 方法:Method getDeclaredMethod(String name, 类<?>… parameterTypes)
∙ \bullet ∙ 参数:
name参数 是一个String ,它指定了所需方法的简单名称;
parameterTypes参数 是 形式参数的类的Class对象 的数组。
∙ \bullet ∙ 返回值:返回一个 方法类对象,它反映此表示的类或接口的指定声明的方法类对象
∙ \bullet ∙ 例:
Method method3 = c.getDeclaredMethod(“method3”, String.class, int.class);- 调用方法:由方法类对象 发起 运行时类对象的该方法的调用
通过方法类对象调用invoke()方法,
∙ \bullet ∙ 调用私有方法前需要先设置访问属性
例:method.invoke(obj,agrs)
反射获取成员方法并使用:
/** * 反射获取成员方法并使用 * 步骤: * 1.获取Class类对象c * 2.使用构造器创建对象obj,即:创建和初始化构造函数的声明类 的新实例。 * 3.通过对象c获得Method类的method对象 * 4.通过对象method调用invoke()方法,method.invoke(obj,agrs) */
public class ReflectionDemo06 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//1.获取Class类对象c
Class<?> c = Class.forName("book.ch04.reflection.Student");
//2.使用构造器创建对象obj
Constructor<?> con = c.getConstructor();
Object obj = con.newInstance();
//3.通过对象c获得Method类的对象method
//Method getDeclaredMethod(String name, 类<?>... parameterTypes):返回一个 方法对象,它反映此表示的类或接口的指定声明的方法 类对象。
Method function = c.getDeclaredMethod("function");
Method method1 = c.getDeclaredMethod("method1");
//void method2(String s)
Method method2 = c.getDeclaredMethod("method2", String.class);
//String method3(String s,int i)
Method method3 = c.getDeclaredMethod("method3", String.class, int.class);
//4.通过对象method调用invoke()方法,
//Object invoke(Object obj, Object... args):在具有指定参数的 方法对象上 调用 此方法对象表示的底层方法。
function.setAccessible(true);
function.invoke(obj);//function
method1.setAccessible(true);
method1.invoke(obj);//method1
method2.invoke(obj, "reflet");//method2(String s)
Object invoke3 = method3.invoke(obj, "reflet", 666);//method3(String s,int i)
System.out.println(invoke3);
}
}
▶ \blacktriangleright ▶反射之越过泛型检查
/** * 如何在ArrayList<integer>集合array中 添加一个字符串数据 * */
public class ReflectionTest01 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//创建ArrayList对象array
ArrayList<Integer> array= new ArrayList<Integer>();
//1.方式二:获得Class对象
Class<? extends ArrayList> c = array.getClass();
//3.获得ArrayList的add方法对象
Method m = c.getMethod("add", Object.class);
//4.创建反射所属类的对象,用来调用方法
//已创建 对象array
//6.通过对象method调用invoke()方法,method.invoke(obj,agrs)
//array.add()
m.invoke(array,10);
m.invoke(array,10);
m.invoke(array,"hello");
m.invoke(array,4.14159);
System.out.println(array);
}
}
>>>sout
>[10, 10, hello, 4.14159]
▶ \blacktriangleright ▶ 反射之运行配置文件指定内容
第18章 新特性
18.1 lambda表达式:重写了函数式接口中的抽象方法的 匿名实现类的 对象
-
当需要提供一个函数式接口的实例时,我们可以使用匿名内部类、lambda表达式、方法引用提供此实例。
-
一个对象是函数式接口的实例 ⇔ \lrArr ⇔ 该对象就可以用Lambda表达式来表示。
1. 函数式接口
▶ \blacktriangleright ▶ lambda 表达式
- Lambda表达式的本质:
作为函数式接口的实例- 格式:形参列表->Lambda体
∙ \bullet ∙ ->:Lambda操作符或箭头操作符
∙ \bullet ∙ ->左边:Lambda形参列表(其实就是接口中的抽象方法的形参列表)
∙ \bullet ∙ ->右边:Lambda体(其实就是重写的抽象方法的方法体)- Lambda表达式的使用:
∙ \bullet ∙ ->左边:
Lambda形参列表的参数类型可以省略(类型推断);
如果Lambda形参列表只有一个参数,() 可以省略
∙ \bullet ∙ ->右边:
Lambda体应该使用一对 { } 包裹;
如果Lambda体只有一条执行语句(可能是return语句),{return }可以省略
▶ \blacktriangleright ▶ 函数式接口
- 定义:
如果一个接口中,只声明了一个抽象方法
,则此接口称为函数式接口。- 四大函数式接口
/** * 四大函数式接口 * 消费性 Consumer<T> void accept(T t) * 供给型 Supplier<T> T get() * 函数型 Function<T,R> R apply(T t) * 断定型 Predicate<T> boolean Test(T t) */
public class lambdaTest {
//消费型接口
@Test
public void test1() {
//直接使用匿名内部类
new Consumer<Double>() {
@Override
public void accept(Double m) {
System.out.println("消费" + m);
}
}.accept(100.0);
//调用方法使用匿名内部类
method(500, new Consumer<Double>() {
@Override
public void accept(Double m) {
System.out.println("消费" + m);
}
});
//直接使用lambda表达式
Consumer<Double> con = (money) -> System.out.println("消费" + money);
con.accept(1500.0);
//调用方法使用lambda表达式
method(2000.0, (money) -> System.out.println("消费" + money));
}
public void method(double money, Consumer<Double> con) {
con.accept((money));
}
---------------------------------------------------------------------------
//断定型
@Test
public void Test2() {
List<String> list = Arrays.asList("北京", "南京", "天津", "pujing");
//调用方法使用匿名内部类
List<String> filterList = filterString(list, new Predicate<String>() {
@Override
public boolean test(String s) {
return s.contains("京");
}
});
System.out.println(filterList);
//调用方法使用lambda表达式
List<String> filterList2 = filterString(list, s -> s.contains("天"));
System.out.println(filterList2);
}
//根据给的规则过滤集合中的字符串,规则由Predicate的test()方法决定
public ArrayList<String> filterString(List<String> list, Predicate<String> pre) {
//保存选出的字符串
ArrayList<String> filterList = new ArrayList<>();
//过滤集合
for (String s : list) {
if (pre.test(s)) {
filterList.add(s);
}
}
return filterList;
}
}
2. 方法引用 :使用现有方法实现对函数式接口中抽象方法的替换重写
。本质:函数式接口匿名实现类的对象
▶ \blacktriangleright ▶ 方法引用的使用
- 定义:
本质上就是Lambda表达式,函数式接口匿名实现类的对象
- 使用:
Lambda体只有一句语句
,并且是通过调用一个对象/类的现有方法
来完成的
即:当要传递给Lambda体的操作,已经有实现的方法了- 使用格式:
类\对象::方法名
- 方法引用要求:
函数式接口的抽象方法的参数列表和返回值 与 引用的方法的参数列表和返回值形式一致。
/** * 方法引用的使用 * 1.使用情境:当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用! * 2.方法引用,本质上就是Lambda表达式,而Lambda表达式作为函数式接口的实例。所以方法引用,也是函数式接口的实例。 * * 3.使用格式:类(或对象)::方法名 * * 4.具体分为如下的三种情况: * 对象::非静态方法 * 类::静态方法 * 类::非静态方注 * * 5.方法引用要求:函数式接口的抽象方法的参数列表和返回值 与方法引用的参数列表和返回值形式一致 */
public class lambdaTest1 {
//情况1: 对象::非静态方法
@Test
public void test1() {
//例1:
//Consumer中的void accept(T t)
//PrintStream中的void println(T t)
Consumer<String> con = str -> System.out.println(str);
con.accept("北京");
Consumer<String> con1 = System.out::println;
con1.accept("beijin");
//例2:
//Supplier中的T get()
// Employee中的:String getName()、double getSalary()、LocalDate getHireDay()*/
Employee alice1 = new Employee("Alice Adams", 75000, 1987, 12, 15);
Supplier s1 = () -> alice1.getName();
System.out.println(s1.get());
Supplier s2 = alice1::getSalary;
System.out.println(s2.get());
Supplier s3 = alice1::getHireDay;
System.out.println(s3.get());
}
//情况2:类::静态方法
@Test
public void test2() {
//例:
//Comparator中 int compare(T t1,T t2)
//Integer中的 int compare(T t1,T t2)
Comparator<Integer> con = (t1, t2) -> Integer.compare(t1, t2);
System.out.println(con.compare(10, 12));
Comparator<Integer> con1 = Integer::compare;
System.out.println(con1.compare(12, 9));
}
//情况3:类::非静态方注
@Test
public void test3() {
//例1:
//Comparator中 int compare(T t1,T t2)
//String中的 int compareTo(T t2)
Comparator<String> con = (t1, t2) -> t1.compareTo(t2);
System.out.println(con.compare("aaa", "bbb"));
Comparator<String> con2 = String::compareTo;
System.out.println(con.compare("aaa", "bbb"));
Employee e = new Employee("Boo ssanm", 75000, 1987, 12, 15);
//例2:
//Function中 R apply(T t)
//Employee中 String getName()
Function<Employee, String> func = Employee::getName;
System.out.println(func.apply(e));//理解:参数作为方法的调用者,将apply的参数Employee e,当作类对象调用非静态方法
}
}
3. 构造器引用
▶ \blacktriangleright ▶ 构造器引用
- 定义:
和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。- 返回值:
抽象方法的返回值类型即为构造函数所属类的类型- 格式:
数组类型名::new
/** * 一、构造器引用 * 和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。 * 抽象方法的返回值类型即为构造函数所属类的类型 * 1.Suplier<T>中的 T get() * 2.Function<T,R>中的 R apply(T t) * 3.BiFunction中的 R apply(T t,U u) * * 二、数组引用 * 将数组看作是一个特殊的类,则写法与构造器引用一致 */
public class ConstructorRefTest {
//Supplier中的T get()
//public Employee()
@Test
public void test1() {
Supplier<Employee> sup = new Supplier<Employee>() {
@Override
public Employee get() {
return new Employee();
}
};
sup.get();
Supplier<Employee> sup2 = () -> new Employee();
sup2.get();
Supplier<Employee> sup3 = Employee::new;
sup3.get();
}
//Function<T,R> R apply(T t)
//public Employee(String name)
@Test
public void test2() {
Function<String, Employee> sup = new Function<String, Employee>() {
@Override
public Employee apply(String name) {
return new Employee(name);
}
};
System.out.println(sup.apply("Alice"));
Function<String, Employee> sup2 = name -> new Employee(name);
System.out.println(sup2.apply("Adams"));
Function<String, Employee> sup3 = Employee::new;
System.out.println(sup3.apply("Alice Adams"));
}
//BiFunction<T,U,R> R apply(T t,U u)
//public Employee(String name,double salary)
@Test
public void test3() {
BiFunction<String, Double, Employee> sup = new BiFunction<String, Double, Employee>() {
@Override
public Employee apply(String s, Double d) {
return new Employee(s, d);
}
};
System.out.println(sup.apply("Alice", 500.0));
BiFunction<String, Double, Employee> sup2 = (s, d) -> new Employee(s, d);
BiFunction<String, Double, Employee> sup3 = Employee::new;
System.out.println(sup3.apply("Blice", 100.0));
}
//数组引用
//Function中的R apply(T t)
@Test
public void test4(){
//1.
Function<Integer,Employee[]> func1 = new Function<Integer, Employee[]>() {
@Override
public Employee[] apply(Integer length) {
return new Employee[length];
}
};
System.out.println(func1.apply(10).length);
//2.
Function<Integer,Employee[]> func2 = Employee[] :: new;
System.out.println(func2.apply(20).length);
}
}
18.2 Stream APl
▶ \blacktriangleright ▶ Stream APl
- 定义:
简言之,Stream API提供了一种高效且易于使用的处理数据的方式。- 为什么要使用Stream APl
NoSQL的数据就需要Java层面去处理。Stream是有关计算的。- 注意:
① Stream自己不会存储元素。
② Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream。
③ Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
1. 创建 Stream
▶ \blacktriangleright ▶ 创建Stream的实例化
- 通过集合 Collection.stream()
- 通过数组 Arrays.stream(staff)
- 通过stream类 of()方法 Stream.of(1, 2, 3, 4, 5, 6)
- 创建无限流
/** *创建Stream的实例化 * 方式一:通过集合 Collection.stream() * 方式二:通过数组 Arrays.stream(staff) * 方式三:通过stream of()方法 Stream.of(1, 2, 3, 4, 5, 6) * 方法四:创建无限流 * */
public class StreamAPITest {
//方式一:通过集合
@Test
public void test1(){
List<Book> books = new ArrayList();
//添加图书
books.add(new Book("book1", 5, "A"));
books.add(new Book("book2", 10, "B"));
books.add(new Book("book3", 20, "C"));
books.add(new Book("book4", 8, "D"));
//顺序流
Stream<Book> stream = books.stream();
//并行流
Stream<Book> bookStream = books.parallelStream();
}
//方式二:通过数组
@Test
public void test2(){
Employee[] staff = new Employee[3];
staff[0] = new Employee("H", 35000);
staff[1] = new Employee("Cracker", 75000);
staff[2] = new Employee("Tony Tester", 38000);
//Arrays类的static stream方法:
//Stream<Employee> stream1 = Arrays.stream(staff);
}
//方式三:通过stream of()方法
@Test
public void test3(){
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
}
//方法四:创建无限流
@Test
public void test4(){
//迭代:static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
//例子:遍历前十个偶数
Stream.iterate(0,i->i+2).limit(10).forEach(i->System.out.println(i));
Stream.iterate(0,i->i+2).limit(10).forEach(System.out::println);
//生成:static <T> Stream<T> generate(Supplier<T> s) 返回无限顺序无序流
Stream.generate(Math::random).limit(10).forEach(System.out::println);
}
}
▶ \blacktriangleright ▶ Stream的中间操作
- 筛选与切片
- 映射
- 排序
/** Stream的中间操作 一、筛选与切片 1.筛选: Stream<T> filter(Predicate p):返回由与此给定谓词匹配的此流的元素组成的流。 2.截断流:Stream<T> limit(long maxSize):返回由此流的元素组成的流,截短长度不能超过 maxSize 。 3.跳过元素:Stream<T> skip(long n):在丢弃流的第一个 n元素后,返回由该流的 n元素组成的流。 4.筛选: Stream<T> distinct():返回由该流的不同元素(根据 Object.equals(Object) )组成的流。 二、映射 1.map 2.flatMap 三、排序 1.自然排序:Stream<T> sorted():返回由此流的元素组成的流,根据自然顺序排序。 2.定制排序:Stream<T> sorted(Comparator<T> com):返回由该流的元素组成的流,根据提供的 Comparator进行排序。 */
public class StreamAPITest1 {
//一、筛选与切片
//1.筛选: Stream<T> filter(Predicate p):返回由与此给定谓词匹配的此流的元素组成的流。
//2.截断流:Stream<T> limit(long maxSize):返回由此流的元素组成的流,截短长度不能超过 maxSize 。
//3.跳过元素:Stream<T> skip(long n):在丢弃流的第一个 n元素后,返回由该流的 n元素组成的流。
//4.筛选: Stream<T> distinct():返回由该流的不同元素(根据 Object.equals(Object) )组成的流。
@Test
public void test1() {
//通过集合生成stream
List<Book> list = BookData.getBooks();
Stream<Book> stream = list.stream();
//1.过滤流 stream.filter()
stream.filter(new Predicate<Book>() {
//参数:断定型 Predicate<T> boolean Test(T t)
@Override
public boolean test(Book book) {
return book.getPrice() == 5;
}
}).forEach(System.out::println);
System.out.println("----------------");
//list.stream().filter(b->b.getAuthor()=="A").forEach(System.out::println);
//2.截断流:Stream<T> limit(long maxSize):返回由此流的元素组成的流,截短长度不能超过 maxSize 。
list.stream().limit(2).forEach(System.out::println);
//list.stream().filter((b)->b.getAuthor()=="A").limit(2).forEach(System.out::println);
//3.跳过元素:Stream<T> skip(long n):在丢弃流的第一个 n元素后,返回由该流的 n元素组成的流。
list.stream().skip(2).forEach(System.out::println);
System.out.println("--------------------");
//4.筛选: Stream<T> distinct():返回由该流的不同元素(根据 Object.equals(Object) )组成的流。需要有hashCode()和 equals()方法
list.add(new Book("book8", 5, "D"));
list.add(new Book("book8", 5, "DA"));
list.stream().distinct().forEach(System.out::println);
}
//二、映射
@Test
public void test2() {
List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
//1.map
//<R> Stream<R> map(Function<T, R> mapper):返回流,返回由给定函数应用于此流的元素的结果组成的流。
//例1:函数toUpperCase()
//list.stream().map(str->str.toUpperCase()).forEach(System.out::println);
//例2:获取类的某一个属性的流
List<Book> books = BookData.getBooks();
//对该属性流进行操作
//books.stream().map(Book::getName).filter(i->i.length()>3).forEach(System.out::println); //方法引用3:类::非静态方注,Function中R apply(T t),把参数t当作类对象调用方法
//例3:map与flatMap
//2.flatMap
//<R> Stream<R> flatMap(Function<T,R> mapper):返回由通过将提供的映射函数应用于每个元素而产生的映射流的内容来替换该流的每个元素的结果的流。
Stream<Stream<Character>> streamStream = list.stream().map(StreamAPITest1::fromStringToStream);
//由stream构成的streamStream,如:{
{a,a},{b,b},{c,c},{d,d}}
streamStream.forEach(s -> s.forEach(System.out::println));//s相当于流的迭代
Stream<Character> characterStream = list.stream().flatMap(StreamAPITest1::fromStringToStream);
//flatMap会打散,如:{a,a,b,b,c,c,d,d}
characterStream.forEach(System.out::println);
}
//一个集合含多个字符串,将字符串中的多个字符转换为对应的Stream实例
public static Stream<Character> fromStringToStream(String str) {
ArrayList<Character> clist = new ArrayList<>();
for (Character c : str.toCharArray()) {
clist.add(c);
}
return clist.stream();
}
//三、排序
@Test
public void test3() {
//1.自然排序:
//Stream<T> sorted():返回由此流的元素组成的流,根据自然顺序排序。
//2.定制排序:
//Stream<T> sorted(Comparator<T> com):返回由该流的元素组成的流,根据提供的 Comparator进行排序。
List<Book> books = BookData.getBooks();
books.stream().sorted((e1, e2) -> Integer.compare(e1.getPrice(), e2.getPrice())).forEach(System.out::println);
}
}
▶ \blacktriangleright ▶ Stream的终止操作
- 匹配与查找
- 归约
- 收集
/** Stream的终止操作 一、匹配与查找 1.allMatch(Predicate p):检查是否匹配所有元素 2.anyMatch(Predicate p):检查是否至少匹配一个元素 3.noneMatch(Predicate p):检查是否没有匹配的元素,是为没有匹配,否为有匹配 4.findFirst():返回第一个元素 5.findAny():返回流中任意元素 6.返回流中元素的总个数 7.max(Comparator c):返回最大值 8.min(Comparator c):返回最小值 8.forEach():内部迭代 二、归约 1.T reduce(T identity, BinaryOperator<T> accumulator):可以将流中元素反复结合起来,返回T 2.Optional<T> reduce(BinaryOperator<T> accumulator):可以将流中元素反复结合起来,返回Optional<T> 参数:BiFunction<T, U, R>中 R apply(T t, U u) 三、收集 1.collect(Collector c):将流转换为其他形式。接收一个Collectior接口的实现 * */
public class StreamAPITest2 {
//一、匹配与查找
@Test
public void test1() {
List<Book> books = BookData.getBooks();
//1.allMatch(Predicate p):检查是否匹配所有元素
boolean p1 = books.stream().allMatch(b -> b.getPrice() >= 5);
//2.anyMatch(Predicate p):检查是否至少匹配一个元素
boolean p2 = books.stream().anyMatch(b -> b.getPrice() >= 50);
//System.out.println(p2);
//3.noneMatch(Predicate p):检查是否没有匹配的元素,是为没有匹配,否为有匹配
boolean p3 = books.stream().noneMatch(b -> b.getName() == "b1");
//System.out.println(p3);
//4.findFirst():返回第一个元素
Optional<Book> first = books.stream().findFirst();
//System.out.println(first);
//5.findAny():返回流中任意元素
Optional<Book> any = books.stream().findAny();
//System.out.println(any);
//6.返回流中元素的总个数
long count = books.stream().filter(book -> book.getName().length() > 2).count();
//System.out.println(count);
//7.max(Comparator c):返回最大值
Stream<Integer> priceStream = books.stream().map(Book::getPrice);//方法引用3:类::非静态方注,Function中R apply(T t),把参数t当作类对象调用方法
Optional<Integer> max = priceStream.max(Integer::compare);
//System.out.println(max);
//8.min(Comparator c):返回最小值
Optional<Integer> min = books.stream().map(Book::getPrice).min(Integer::compare);
//System.out.println(min);
//8.forEach():内部迭代
books.stream().forEach(System.out::println);
}
//二、归约
@Test
public void test2() {
//1.T reduce(T identity, BinaryOperator<T> accumulator)
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
//参数:BiFunction<T, U, R>中 R apply(T t, U u)
Integer reduce1 = list.stream().reduce(0, (a, b) -> a + b);//参数:BiFunction<T, T, T>
Integer reduce2 = list.stream().reduce(10, Integer::sum);//参数:BiFunction<T, T, T>
//System.out.println(reduce2);
//2.Optional<T> reduce(BinaryOperator<T> accumulator)
List<Book> books = BookData.getBooks();
Optional<Integer> reduce3 = books.stream().map(Book::getPrice).reduce(Integer::sum);
System.out.println(reduce3);
}
//三、收集
@Test
public void test3() {
//1.collect(Collector c):将流转换为其他形式。接收一个Collectior接口的实现
List<Book> books = BookData.getBooks();
List<Book> collect = books.stream().filter(b -> b.getPrice() > 10).collect(Collectors.toList());
collect.forEach(System.out::println);
}
}
今天的文章阶段1 Java入门「建议收藏」分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/88522.html