写在这里,建议大家转往语雀进行观看,这部分知识不够全面,地址如下:
https://www.yuque.com/docs/share/858be01e-6325-42e6-b5a9-7a40e37babf3?# 《JavaSE》
另外,本人将持续更新语雀平台知识库,希望喜欢的小伙伴可以收藏。
知识库地址:
https://www.yuque.com/books/share/ffbf2b86-52af-4a4f-b144-507a890fb8dc?# 《Teng的计算机知识库》
1.数据类型转换的原理
自动类型转换
-
自动转换:将取值范围小的类型自动提升为取值范围大的类型
-
转换原理图解:
byte
类型内存占有1个字节,在和int
类型运算时会提升为int
类型 ,自动补充3个字节,因此计算后的结果还是int
类型。
同样道理,当一个**int
类型变量和一个double
变量运算时,int
类型将会自动提升为double
**类型进行运算
-
**转换规则:**范围小的类型向范围大的类型提升,
byte、short、char
运算时直接提升为int
。byte、short、char‐‐>int‐‐>long‐‐>float‐‐>double
强制转换
-
强制转换原理图解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xnRZxJ4X-1660922686061)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\01.java基础语法\02. 【数据类型转换、运算符、方法入门】\03_参考\01-强制类型转换的数据溢出问题.png)]
方法重载
- 方法重载: 指在同一个类中,允许存在一个以上的同名方法,参数列表不同(参数类型不同或者参数个数不同或者顺序不同),方法的返回值类型可以不同,方法的修饰符也可以不同;main方法也可以被重载
- 参数列表:个数不同,数据类型不同,顺序不同。
- 重载方法调用:JVM通过方法的参数列表,调用不同的方法。
main方法的参数列表中的String[] args是什么?
- 1.String[] args是专门用来接收命令行参数的:
命令行参数:
如:在一个主类中,运行该程序时: java Test1 365 156 “China”
后面所谓的365 156 "China"就是命令行参数
JVM在调用Array类的main方法之前,先将365 156 "China"这个字符以"空格"的方式分割,然后存储在String数组中。
public class Test {
public static void main(String[] args) {
System.out.println("String类型中数组的元素个数是:" + args.length);
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
}
// 下面为运行测试
javac Test.java
java Test 365 156 "China"
String类型中数组的元素个数是:3
365
156
China
- 该方法的作用: 现在有这样一个需求:
/
需求说明:运行该软件的时候必须提供用户名和密码
格式:java Array username password
如果没有提供足够的参数,则退出系统
/
那么该如何实现其功能呢?
分析一波,ememmemem…
首先,我们所要的为username and password,这也就说明我们所需要的String[]args数组的个数为2.
接下来,如果参数提供正确且用户名为admin,密码为123,则登陆成功。
public class Array{
public static void main(String[]args){
if (args.length!=2){
System.out.println("要想使用该系统,必须这样输入: java username password");
return;
}
//参数提供正确,如果用户名是admin,密码是123,则登陆成功
String username = args[0];
String password = args[1];
//java中比较字符串是否相等,必须使用equals方法
//String类型是SUN公司提供,已经重写了equals方法,比较的是内容。
if ("admin".equals(username) && "123".equals(password)){//用这种方式写避免了空指针异常
//if(username.equals("admin") && password.equals("123")) {
System.out.println("登陆成功.欢迎["+username+"]回来.");
}else {
System.out.println("登录失败,用户名["+username+"]不存在或者密码错误!");
}
}
}
2.数组
java虚拟机的内存划分
为了提高运算效率,就对空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。
-
堆:此内存区域的唯一目的 就是存放对象实例,几乎所有的对象 实例都在这里分配内存。这一点在 Java虚拟机规范中的描述是:所有的 对象实例以及数组都要在堆上分配
-
栈:指虚拟机栈。虚拟机栈用于存储局部变量等。 局部变量表存放了编译期可知长度的各种基本数据类型(boolean、byte、 char 、 short 、 int 、 float 、 long 、double)、对象引用(reference类型, 它不等同于对象本身,是对象在堆内存的首地址)。方法执行完,自动释放。
-
方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
-
JVM的内存划分:
区域名称 作用 寄存器 给CPU使用,和我们开发无关。 本地方法栈 JVM在使用操作系统功能的时候使用,和我们开发无关。 方法区 存储可以运行的class文件。(有常量池和静态域) 堆内存 存储对象或者数组,new来创建的,都存储在堆内存。 方法栈 方法运行时使用的内存,比如main方法运行,进入方法栈中执行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gSSnbLho-1660922686063)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\01.java基础语法\05.【数组】\03_参考\01-Java中的内存划分.png)]
数组的内存存储
-
数组在内存中的存储
-
一个数组的内存图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OhWz5xZR-1660922686063)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\01.java基础语法\05.【数组】\03_参考\02-只有一个数组的内存图.png)]
-
两个数组的内存图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BI9wM3mw-1660922686064)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\01.java基础语法\05.【数组】\03_参考\03-有两个独立数组的内存图.png)]
-
两个变量指向一个数组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ra56xqPV-1660922686064)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\01.java基础语法\05.【数组】\03_参考\04-两个引用指向同一个数组的内存图.png)]
-
总结: 方法的参数为基本类型时,传递的是数据值. 方法的参数为引用类型时,传递的是地址值.
-
3.类与对象
对象的内存存储
-
只有一个对象的内存图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-52V9osXl-1660922686065)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\02.面向对象和封装\06.【类与对象、封装、构造方法】\03_参考\01-只有一个对象的内存图.png)]
-
两个对象使用同一个方法的内存图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bOox0lJc-1660922686065)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\02.面向对象和封装\06.【类与对象、封装、构造方法】\03_参考\02-两个对象使用同一个方法的内存图.png)]
-
两个引用指向同一个对象的内存图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLSpkswS-1660922686066)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\02.面向对象和封装\06.【类与对象、封装、构造方法】\03_参考\03-两个引用指向同一个对象的内存图.png)]
-
使用对象类型作为方法的参数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WQx2F111-1660922686066)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\02.面向对象和封装\06.【类与对象、封装、构造方法】\03_参考\04-使用对象类型作为方法的参数.png)]
-
使用对象类型作为方法的返回值:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VloEvLO0-1660922686067)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\02.面向对象和封装\06.【类与对象、封装、构造方法】\03_参考\05-使用对象类型作为方法的返回值.png)]
成员变量和局部变量的区别
- 在类中的位置不同
重点
- 成员变量:类中,方法外
- 局部变量:方法或者方法声明上(形式参数)
- 作用范围不一样
重点
- 成员变量:类中
- 局部变量:方法中
- 初始化的值不同
重点
- 成员变量:有默认值
- 局部变量:没有默认值。必须先定义,赋值,最后使用
- 在内存中的位置不同
- 成员变量:堆内存
- 局部变量:栈内存
- 成名周期不同
- 成员变量:随着对象的创建而存在,随着对象的消失而消失
- 局部变量:随着方法的调用而存在,随着方法的调用完毕而消失
static关键字
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qDFRG2kW-1660922686069)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\03.常用API第一部分\08.【String类、static、Arrays类、Math类】\03_参考\02-静态static关键字概述.png)]
-
静态方法调用的注意事项:
- 静态方法可以直接访问类变量和静态方法。
- 静态方法不能直接访问普通成员变量或成员方法。反之,成员方法可以直接访问类变量或静态方法。
- 静态方法中,不能使用this关键字。
-
静态原理图解:
static
修饰的内容:-
是随着类的加载而加载的,且只加载一次。
-
存储于一块固定的内存区域(静态区),所以,可以直接被类名调用。
-
它优先于对象存在,所以,可以被所有对象共享。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ffDiZpap-1660922686073)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\03.常用API第一部分\08.【String类、static、Arrays类、Math类】\03_参考\03-静态的内存图.png)]
-
-
**静态代码块:**定义在成员位置,使用static修饰的代码块{ }。
- 位置:类中方法外。
- 执行:随着类的加载而执行且执行一次,优先于main方法和构造方法的执行。
- 先静态代码块,再普通代码块,再构造器。
4.常用API第一部分
Random类与ArrayList类
- Random类的常用方法:
public int nextInt(int n)
:返回一个伪随机数,范围在0
(包括)和 指定值n
(不包括)之间的int
值。 - ArrayList类的常用方法:
public boolean add(E e)
:将指定的元素添加到此集合的尾部。public E remove(int index)
:移除此集合中指定位置上的元素。返回被删除的元素。public E get(int index)
:返回此集合中指定位置上的元素。返回获取的元素。public int size()
:返回此集合中的元素数。遍历集合时,可以控制索引范围,防止越界。
- 注意:ArrayList对象不能存储基本类型,只能存储引用类型的数据!!!
String类、Arrays类、Math类
1.String
-
字符串特点:
-
1.字符串不变:字符串的值在创建后不能被更改。
String s1 = "abc"; s1 += "d"; System.out.println(s1); // "abcd" // 内存中有"abc","abcd"两个对象,s1从指向"abc",改变指向,指向了"abcd"。
-
2.因为String对象是不可变的,所以它们可以被共享。
String s1 = "abc"; String s2 = "abc"; // 内存中只有一个"abc"对象被创建,同时被s1和s2共享。
-
3.
"abc"
等效于char[] data={ 'a' , 'b' , 'c' }
。例如: String str = "abc"; 相当于: char data[] = {'a', 'b', 'c'}; String str = new String(data); // String底层是靠字符数组实现的。
-
-
字符串内存图解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-czFrf9sB-1660922686074)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\03.常用API第一部分\08.【String类、static、Arrays类、Math类】\03_参考\01-字符串的常量池.png)]
-
字符串常用功能:
- 判断功能的方法:
public boolean equals (Object anObject)
:将此字符串与指定对象进行比较。public boolean equalsIgnoreCase (String anotherString)
:将此字符串与指定对象进行比较,忽略大小写。
- 获取功能的方法:
public int length ()
:返回此字符串的长度。public String concat (String str)
:将指定的字符串连接到该字符串的末尾。public char charAt (int index)
:返回指定索引处的 char值。public int indexOf (String str)
:返回指定子字符串第一次出现在该字符串内的索引。public String substring (int beginIndex)
:返回一个子字符串,从beginIndex开始截取字符串到字符 串结尾。public String substring (int beginIndex, int endIndex)
:返回一个子字符串,从beginIndex到 endIndex截取字符串。含beginIndex,不含endIndex。
- 转换功能的方法:
public char[] toCharArray ()
:将此字符串转换为新的字符数组。public byte[] getBytes ()
:使用平台的默认字符集将该 String编码转换为新的字节数组。public String replace (CharSequence target, CharSequence replacement)
:将与target匹配的字符串使 用replacement字符串替换。
- 分割功能的方法:
public String[] split(String regex)
:将此字符串按照给定的regex(规则)拆分为字符串数组。
- 判断功能的方法:
2.Arrays
public static String toString(int[] a)
:返回指定数组内容的字符串表示形式。public static void sort(int[] a)
:对指定的 int 型数组按数字升序进行排序。
3.Math
public static double abs(double a)
:返回 double 值的绝对值。public static double ceil(double a)
:返回大于等于参数的最小的整数。public static double floor(double a)
:返回小于等于参数最大的整数。public static long round(double a)
:返回最接近参数的 long。(相当于四舍五入方法)
5.继承、super、this、抽象类
继承
- 覆盖的注意事项:
-
- 子类方法覆盖父类方法,必须要保证权限大于等于父类权限。
-
- 子类方法覆盖父类方法,返回值类型、函数名和参数列表都要一模一样。
-
super与this
-
super与this的内存图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pjWYeC2C-1660922686075)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\04.继承与多态\09.【继承、super、this、抽象类】\03_参考\03-super与this的内存图.png)]
抽象类
- 抽象类的注意事项:
-
- 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。 理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
-
- 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。 理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。
-
- 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。
- 4.抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。 理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。
-
6.接口与多态
接口
-
接口中含有抽象方法
抽象方法:使用
abstract
关键字修饰,可以省略,没有方法体。该方法供子类实现使用。代码如下:
public interface InterFaceName { public abstract void method(); }
-
接口中含有默认方法和静态方法
默认方法:使用
default
修饰,不可省略,供子类调用或者子类重写。静态方法:使用
static
修饰,供接口直接调用。代码如下:
public interface InterFaceName { public default void method() { // 执行语句 } public static void method2() { // 执行语句 } }
-
接口中含有私有方法
私有方法:使用
private
修饰,供接口中的默认方法或者静态方法调用。代码如下:
public interface InterFaceName { private void method() { // 执行语句 } }
-
接口的基本实现
非抽象子类实现接口:
- 必须重写接口中所有抽象方法。
- 继承了接口的默认方法,即可以直接调用,也可以重写。
实现格式:
class 类名 implements 接口名 { // 重写接口中抽象方法【必须】 // 重写接口中默认方法【可选】 }
-
接口中其他成员的特点
- 接口中,无法定义成员变量,但是可以定义常量,其值不可以改变,默认使用
public static final
修饰。 - 接口中,没有构造方法,不能创建对象。
- 接口中,没有静态代码块。
- 接口中,无法定义成员变量,但是可以定义常量,其值不可以改变,默认使用
关于接口中的回调问题
/*
* AAA类的方法,调用BBB类的方法,BBB类的方法在执行完后,又调用AAA类的方法
* 这就是方法回调。
*
* 如果有多个类的方法都要调用BBB类的方法,
* BBB类的方法在执行完后,要针对不同的类回调,这时就要采用接口的方式,就叫做接口回调。
*/
//一个简单的方法回调Demo
public class CallBackDemo1 {
public static void main(String[] args) {
AAA aaa = new AAA();
aaa.aMethod();
}
}
class AAA {
public void aMethod() {
BBB bbb = new BBB();
int n = 8;
bbb.bMethod(n, this);
}
public void callBack(String result) {
if (result.equals("ok")) {
System.out.println(Thread.currentThread().getName() + "成功!");
}
}
}
class BBB {
public void bMethod(int n, AAA aaa) {
new Thread(() -> {
int i = 0;
while (i < n) {
System.out.println(Thread.currentThread().getName() + "操作次数:" + i);
++i;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//方法回调
aaa.callBack("ok");
}).start();
}
}
// 如果有多个类的方法都要调用BB1类的方法,
// BB1类的方法在执行完后,要针对不同的类回调,这时就要采用接口的方式,就叫做接口回调。
// 多个类的调用同一个类的方法返回不同的结果---需要多个类实现一个公共接口
public class CallBackDemo2 {
public static void main(String[] args) {
AA1 aa1 = new AA1();
aa1.aMethod();
AA2 aa2 = new AA2();
aa2.aMethod();
}
}
//回调接口
interface Call {
void callBack(String result);
}
class AA1 implements Call {
public void aMethod() {
BB1 bb1 = new BB1();
int n = 10;
bb1.bMethod(n, this);
}
@Override
public void callBack(String result) {
if (result.equals("yes")) {
System.out.println(Thread.currentThread().getName() + "成功!");
}
}
}
class AA2 implements Call {
public void aMethod() {
BB1 bb1 = new BB1();
int n = 5;
bb1.bMethod(n, this);
}
@Override
public void callBack(String result) {
if (result.equals("yes")) {
System.out.println(Thread.currentThread().getName() + "失败!");
}
}
}
class BB1 {
public void bMethod(int n, Call call) {
new Thread(() -> {
int i = 0;
while (i < n) {
System.out.println(Thread.currentThread().getName()+"操作次数:" +i);
++i;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//针对不同的实现类调用不同的结果
call.callBack("yes");
}).start();
}
}
// 当多个AAA类的对象,调用BBB类的方法,
// BBB类的方法在执行完后,又要采用AAA类中不同的回应的方法 采用抽象方法
// 这时该类为抽象类 使用时需要用匿名内部类或者lambda表达式
public class CallBackDemo3 {
public static void main(String[] args) {
//匿名内部类
AA3 aa3 = new AA3() {
@Override
public void callBack(String result) {
if (result.equals("ok")) {
System.out.println(Thread.currentThread().getName() + "成功");
}
}
};
aa3.aMethod();
AA3 aa31 = new AA3() {
@Override
public void callBack(String result) {
if (result.equals("ok")) {
System.out.println(Thread.currentThread().getName() + "失败");
}
}
};
aa31.aMethod();
}
}
abstract class AA3 {
public void aMethod() {
BB2 bb2 = new BB2();
int n = 5;
bb2.bMethod(n, this);
}
public abstract void callBack(String result);
}
class BB2 {
public void bMethod(int n, AA3 aa3) {
new Thread(() -> {
int i = 0;
while (i < n) {
System.out.println(Thread.currentThread().getName() + "操作次数:" + i);
i++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
aa3.callBack("ok");
}).start();
}
}
/**
* 当AA类两个方法,调用BB类的方法,
* BB类的方法在执行完后,又要采用不同的回应的方法
* 需要回调接口
*/
public class CallBackDemo4 {
public static void main(String[] args) {
AA aa = new AA();
aa.aMethod1(5);
aa.aMethod2(10);
}
}
class AA {
public void aMethod1(int n) {
BB bb = new BB();
bb.bMethod(n, (result) -> {
if (result.equals("ok")) {
System.out.println("成功");
}
});
}
public void aMethod2(int n) {
BB bb = new BB();
bb.bMethod(n, (result) -> {
if (result.equals("ok")) {
System.out.println("失败");
}
});
}
}
interface CallResult {
void callBack(String result);
}
class BB {
public void bMethod(int n, CallResult callResult) {
new Thread(() -> {
int i = 0;
while (i < n) {
System.out.println(Thread.currentThread().getName() + "操作次数:" + i);
++i;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
callResult.callBack("ok");
}).start();
}
}
多态
父类类型 变量名 = new 子类对象;
变量名.方法名();
//父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
-
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写后方法。
-
多态的成员变量:(重点)
当子父类中出现同名的成员变量时,多态调用该变量时:
编译时期:参考的是引用型变量所属的类中是否有被调用的成员变量。没有,编译失败。
运行时期:也是调用引用型变量所属的类中的成员变量。
简单记:编译和运行都参考等号的左边。编译运行都看左边。
-
多态的成员方法:(重点)
编译时期:参考引用变量所属的类,如果没有类中没有调用的方法,编译失败。
运行时期:参考引用变量所指的对象所属的类,并运行对象所属类中的成员方法。
简而言之:编译看左边,运行看右边。
-
引用类型转换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DpeLb7ir-1660922686075)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\04.继承与多态\10.【接口、多态】\03_参考\05-对象的上下转型.png)]
-
向上类型转换:
父类类型 变量名 = new 子类类型(); 如:Animal a = new Cat();
-
向下类型转换:
子类类型 变量名 = (子类类型) 父类变量名; 如:Cat c =(Cat) a;
-
-
转型的异常
为了避免ClassCastException的发生,Java提供了
instanceof
关键字,给引用变量做类型的校验,格式如下:变量名 instanceof 数据类型 如果变量属于该数据类型,返回true。 如果变量不属于该数据类型,返回false。 public class Test { public static void main(String[] args) { // 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat // 向下转型 if (a instanceof Cat){ Cat c = (Cat)a; c.catchMouse(); // 调用的是 Cat 的 catchMouse } else if (a instanceof Dog){ Dog d = (Dog)a; d.watchHouse(); // 调用的是 Dog 的 watchHouse } } }
7.final与权限与内部类
final
-
final: 不可改变。可以用于修饰类、方法和变量。
- 类:被修饰的类,不能被继承。
- 方法:被修饰的方法,不能被重写。
- 变量:被修饰的变量,不能被重新赋值。
-
思考题:下列哪种方法可以通过编译
-
写法一:
final int c = 0; for (int i = 0; i < 10; i++) { c = i; System.out.println(c); }
-
写法二:
for (int i = 0; i < 10; i++) { final int c = i; System.out.println(c); }
根据
final
的定义,写法1报错!写法2,为什么通过编译呢?因为每次循环,都是一次新的变量c。这也是大家 需要注意的地方。
-
权限修饰符
- 不同权限的访问能力
同一个类 | 同一个包 | 不同包的子类 | 不同包的非子类 | |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | |
默认(default) | √ | √ | ||
private | √ |
编写代码时,如果没有特殊的考虑,建议这样使用权限:
- 成员变量使用
private
,隐藏细节。 - 构造方法使用
public
,方便创建对象。 - 成员方法使用
public
,方便调用方法。
内部类
成员内部类
- 成员内部类: 定义在类中方法外的类。
定义格式:
class 外部类 {
class 内部类 {
}
}
访问特点
- 内部类可以直接访问外部类的成员,包括私有成员。
- 外部类要访问内部类的成员,必须要建立内部类的对象。
创建内部类对象格式:
外部类名.内部类名 对象名 = new 外部类型().new 内部类型();
匿名内部类【重点】
- **匿名内部类 :**是内部类的简化写法。它的本质是一个
带具体实现的
父类或者父接口的
匿名的
子类对象。 开发中,最常用到的内部类就是匿名内部类了。以接口举例,当你使用一个接口时,似乎得做如下几步操作,
1.定义子类
2.重写接口中的方法
3.创建子类对象
4.调用重写后的方法
我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成一步呢?匿名内部类就是做这样的快捷方式。
前提
匿名内部类必须继承一个父类或者实现一个父接口。
格式
new 父类名或者接口名(){
// 方法重写
@Override
public void method() {
// 执行语句
}
};
使用方法
以接口为例,匿名内部类的使用,代码如下:
定义接口:
public abstract class FlyAble{
public abstract void fly();
}
创建匿名内部类,并调用:
public class InnerDemo {
public static void main(String[] args) {
/*
1.等号右边:是匿名内部类,定义并创建该接口的子类对象
2.等号左边:是多态赋值,接口类型引用指向子类对象
*/
FlyAble f = new FlyAble(){
public void fly() {
System.out.println("我飞了~~~");
}
};
//调用 fly方法,执行重写后的方法
f.fly();
}
}
通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递。代码如下:
public class InnerDemo2 {
public static void main(String[] args) {
/*
1.等号右边:定义并创建该接口的子类对象
2.等号左边:是多态,接口类型引用指向子类对象
*/
FlyAble f = new FlyAble(){
public void fly() {
System.out.println("我飞了~~~");
}
};
// 将f传递给showFly方法中
showFly(f);
}
public static void showFly(FlyAble f) {
f.fly();
}
}
以上两步,也可以简化为一步,代码如下:
public class InnerDemo3 {
public static void main(String[] args) {
/*
创建匿名内部类,直接传递给showFly(FlyAble f)
*/
showFly( new FlyAble(){
public void fly() {
System.out.println("我飞了~~~");
}
});
}
public static void showFly(FlyAble f) {
f.fly();
}
}
接口的其他知识
interface作为成员变量时,对它进行赋值的操作,实际上,是赋给它该接口的一个子类对象。
接口作为参数时,传递它的子类对象。
接口作为返回值类型时,返回它的子类对象。
8.常用API第二部分
Date类
常用方法
public long getTime()
把日期对象转换成对应的时间毫秒值。
DateFormat类
java.text.DateFormat
是日期/时间格式化子类的抽象类,我们通过这个类可以帮我们完成日期和文本之间的转换,也就是可以在Date对象与String对象之间进行来回转换。
- **格式化:**按照指定的格式,从Date对象转换为String对象。
- **解析:**按照指定的格式,从String对象转换为Date对象。
构造方法
由于DateFormat为抽象类,不能直接使用,所以需要常用的子类java.text.SimpleDateFormat
。这个类需要一个模式(格式)来指定格式化或解析的标准。构造方法为:
public SimpleDateFormat(String pattern)
:用给定的模式和默认语言环境的日期格式符号构造SimpleDateFormat。
参数pattern是一个字符串,代表日期时间的自定义格式。
常用方法
DateFormat类的常用方法有:
public String format(Date date)
:将Date对象格式化为字符串。public Date parse(String source)
:将字符串解析为Date对象。
Calendar类
获取方式
Calendar为抽象类,由于语言敏感性,Calendar类在创建对象时并非直接创建,而是通过静态方法创建,返回子类对象,如下:
Calendar静态方法
public static Calendar getInstance()
:使用默认时区和语言环境获得一个日历
import java.util.Calendar;
public class Demo06CalendarInit {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
}
}
常用方法
public int get(int field)
:返回给定日历字段的值。public void set(int field, int value)
:将给定的日历字段设置为给定值。public abstract void add(int field, int amount)
:根据日历的规则,为给定的日历字段添加或减去指定的时间量。public Date getTime()
:返回一个表示此Calendar时间值(从历元到现在的毫秒偏移量)的Date对象。
System类
常用方法
public static long currentTimeMillis()
:返回以毫秒为单位的当前时间。public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
:将数组中指定的数据拷贝到另一个数组中。
数组的拷贝动作是系统级的,性能很高。System.arraycopy方法具有5个参数,含义分别为:
参数序号 | 参数名称 | 参数类型 | 参数含义 |
---|---|---|---|
1 | src | Object | 源数组 |
2 | srcPos | int | 源数组索引起始位置 |
3 | dest | Object | 目标数组 |
4 | destPos | int | 目标数组索引起始位置 |
5 | length | int | 复制元素个数 |
StringBuilder类
原理
StringBuilder又称为可变字符序列,它是一个类似于 String 的字符串缓冲区,通过某些方法调用可以改变该序列的长度和内容。
原来StringBuilder是个字符串的缓冲区,即它是一个容器,容器中可以装很多字符串。并且能够对其中的字符串进行各种操作。
它的内部拥有一个数组用来存放字符串内容,进行字符串拼接时,直接在数组中加入新内容。StringBuilder会自动维护数组的扩容。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XNClVIs4-1660922686076)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\05.常用API第二部分\12.【Object类、常用API】-笔记\resource\01_StringBuilder的原理.bmp)]
常用方法
public StringBuilder append(...)
:添加任意类型数据的字符串形式,并返回当前对象自身。public String toString()
:将当前StringBuilder对象转换为String对象。
包装类
自动拆箱与自动装箱
9.集合
单列集合
-
Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是
java.util.List
和java.util.Set
。其中,List
的特点是元素有序、元素可重复。Set
的特点是元素无序,而且不可重复。List
接口的主要实现类有java.util.ArrayList
和java.util.LinkedList
,Set
接口的主要实现类有java.util.HashSet
和java.util.TreeSet
。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EAYnW0Yu-1660922686076)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\06.集合\13.【Collection、泛型】-笔记\resource\01_集合框架介绍.bmp)]
Collection常用功能
public boolean add(E e)
: 把给定的对象添加到当前集合中 。public void clear()
:清空集合中所有的元素。public boolean remove(E e)
: 把给定的对象在当前集合中删除。public boolean contains(E e)
: 判断当前集合中是否包含给定的对象。public boolean isEmpty()
: 判断当前集合是否为空。public int size()
: 返回集合中元素的个数。public Object[] toArray()
: 把集合中的元素,存储到数组中。
Iterator接口
想要遍历Collection集合,那么就要获取该集合迭代器完成迭代操作,下面介绍一下获取迭代器的方法:
public Iterator iterator()
: 获取集合对应的迭代器,用来遍历集合中的元素的。
Iterator接口的常用方法如下:
public E next()
:返回迭代的下一个元素。public boolean hasNext()
:如果仍有元素可以迭代,则返回 true。
Iterator实现原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HPXDRrP1-1660922686077)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\06.集合\13.【Collection、泛型】-笔记\resource\02_迭代器的实现原理(1)].bmp)
泛型
定义泛型的类
class ArrayList<E>{
public boolean add(E e){ }
public E get(int index){ }
....
}
含有泛型的方法
public class MyGenericMethod {
public <MVP> void show(MVP mvp) {
System.out.println(mvp.getClass());
}
public <MVP> MVP show2(MVP mvp) {
return mvp;
}
}
含有泛型的接口
public interface MyGenericInterface<E>{
public abstract void add(E e);
public abstract E getE();
}
泛型通配符
当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。
通配符的基本使用
泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。
此时只能接受数据,不能往该集合中存储数据。
public static void main(String[] args) {
Collection<Intger> list1 = new ArrayList<Integer>();
getElement(list1);
Collection<String> list2 = new ArrayList<String>();
getElement(list2);
}
public static void getElement(Collection<?> coll){}
//?代表可以接收任意类型
通配符高级使用—受限泛型
之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限和下限。
泛型的上限:
- 格式:
类型名称 <? extends 类 > 对象名称
- 意义:
只能接收该类型及其子类
泛型的下限:
- 格式:
类型名称 <? super 类 > 对象名称
- 意义:
只能接收该类型及其父类型
比如:现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类
public static void main(String[] args) {
Collection<Integer> list1 = new ArrayList<Integer>();
Collection<String> list2 = new ArrayList<String>();
Collection<Number> list3 = new ArrayList<Number>();
Collection<Object> list4 = new ArrayList<Object>();
getElement(list1);
getElement(list2);//报错
getElement(list3);
getElement(list4);//报错
getElement2(list1);//报错
getElement2(list2);//报错
getElement2(list3);
getElement2(list4);
}
// 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(Collection<? extends Number> coll){}
// 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(Collection<? super Number> coll){}
10.List与Set集合
List接口
特点
- 它是一个元素存取有序的集合。例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
- 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
- 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。
常用方法
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:
public void add(int index, E element)
: 将指定的元素,添加到该集合中的指定位置上。public E get(int index)
:返回集合中指定位置的元素。public E remove(int index)
: 移除列表中指定位置的元素, 返回的是被移除的元素。public E set(int index, E element)
:用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
List的子类
ArrayList集合
java.util.ArrayList
集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList
是最常用的集合。
LinkedList集合
java.util.LinkedList
集合数据存储的结构是链表结构。方便元素添加、删除的集合。
了解的方法
public void addFirst(E e)
:将指定元素插入此列表的开头。public void addLast(E e)
:将指定元素添加到此列表的结尾。public E getFirst()
:返回此列表的第一个元素。public E getLast()
:返回此列表的最后一个元素。public E removeFirst()
:移除并返回此列表的第一个元素。public E removeLast()
:移除并返回此列表的最后一个元素。public E pop()
:从此列表所表示的堆栈处弹出一个元素。public void push(E e)
:将元素推入此列表所表示的堆栈。public boolean isEmpty()
:如果列表不包含元素,则返回true。
Set接口
特点
Set
接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。
Set
集合有多个子类,这里我们介绍其中的java.util.HashSet
、java.util.LinkedHashSet
这两个集合。
tips:Set集合取出元素的方式可以采用:迭代器、增强for。
Set的子类
HashSet集合
java.util.HashSet
是Set
接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。java.util.HashSet
底层的实现其实是一个java.util.HashMap
支持
HashSet
是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashCode
与equals
方法。
HashSet存储自定义元素
给HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一
Set集合存储元素不重复的原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sMvzKr59-1660922686077)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\06.集合\14.【List、Set】-笔记\resource\06_Set集合存储元素不重复的原理.bmp)]
LinkedHashSet集合
特点
在HashSet下面有一个子类java.util.LinkedHashSet
LinkedHashSet
集合中的元素是有序的,但是存入的元素不会出现重复
它是链表和哈希表组合的一个数据存储结构。
哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的
可变参数
在JDK1.5之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化成如下格式:
修饰符 返回值类型 方法名(参数类型... 形参名){ }
其实这个书写完全等价与
修饰符 返回值类型 方法名(参数类型[] 形参名){ }
注意:如果在方法书写时,这个方法拥有多参数,参数中包含可变参数,可变参数一定要写在参数列表的末尾位置。
Collections
常用功能
java.utils.Collections
是集合工具类,用来对集合进行操作。部分方法如下:
public static <T> boolean addAll(Collection<T> c, T... elements)
:往集合中添加一些元素。public static void shuffle(List<?> list) 打乱顺序
:打乱集合顺序。public static <T> void sort(List<T> list)
:将集合中元素按照默认规则排序。public static <T> void sort(List<T> list,Comparator<? super T> )
:将集合中元素按照指定规则排序。
Comparator比较器
我们还是先研究这个方法
public static <T> void sort(List<T> list)
:将集合中元素按照默认规则排序。
不过这次存储的是字符串类型。
public class CollectionsDemo2 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("cba");
list.add("aba");
list.add("sba");
list.add("nba");
//排序方法
Collections.sort(list);
System.out.println(list);
}
}
结果:
[aba, cba, nba, sba]
我们使用的是默认的规则完成字符串的排序,那么默认规则是怎么定义出来的呢?
说到排序了,简单的说就是两个对象之间比较大小,那么在JAVA中提供了两种比较实现的方式,一种是比较死板的采用java.lang.Comparable
接口去实现,一种是灵活的当我需要做排序的时候在去选择的java.util.Comparator
接口完成。
那么我们采用的public static <T> void sort(List<T> list)
这个方法完成的排序,实际上要求了被排序的类型需要实现Comparable接口完成比较的功能,在String类型上如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
String类实现了这个接口,并完成了比较规则的定义,但是这样就把这种规则写死了,那比如我想要字符串按照第一个字符降序排列,那么这样就要修改String的源代码,这是不可能的了,那么这个时候我们可以使用
public static <T> void sort(List<T> list,Comparator<? super T> )
方法灵活的完成,这个里面就涉及到了Comparator这个接口,位于位于java.util包下,排序是comparator能实现的功能之一,该接口代表一个比较器,比较器具有可比性!顾名思义就是做排序的,通俗地讲需要比较两个对象谁排在前谁排在后,那么比较的方法就是:
-
public int compare(String o1, String o2)
:比较其两个参数的顺序。两个对象比较的结果有三种:大于,等于,小于。
如果要按照升序排序,前面的减去后面的
如果要按照降序排序,后面的减去前面的
简述Comparable和Comparator两个接口的区别
Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
Comparator强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。
练习
创建一个学生类,存储到ArrayList集合中完成指定排序操作。
Student 初始类
public class Student{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试类:
public class Demo {
public static void main(String[] args) {
// 创建四个学生对象 存储到集合中
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("rose",18));
list.add(new Student("jack",16));
list.add(new Student("abc",16));
list.add(new Student("ace",17));
list.add(new Student("mark",16));
/*
让学生 按照年龄排序 升序
*/
// Collections.sort(list);//要求 该list中元素类型 必须实现比较器Comparable接口
for (Student student : list) {
System.out.println(student);
}
}
}
发现,当我们调用Collections.sort()方法的时候 程序报错了。
原因:如果想要集合中的元素完成排序,那么必须要实现比较器`Comparable接口。
于是我们就完成了Student类的一个实现,如下:
public class Student implements Comparable<Student>{
....
@Override
public int compareTo(Student o) {
return this.age-o.age;//升序
}
}
扩展
如果在使用的时候,想要独立的定义规则去使用 可以采用Collections.sort(List list,Comparetor<T> c)
方式,自己定义规则:
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o2.getAge()-o1.getAge();//以学生的年龄降序
}
});
如果想要规则更多一些,可以参考下面代码:
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 年龄降序
int result = o2.getAge()-o1.getAge();//年龄降序
if(result==0){//第一个规则判断完了 下一个规则 姓名的首字母 升序
result = o1.getName().charAt(0)-o2.getName().charAt(0);
}
return result;
}
});
11.Map集合
Map常用子类
通过查看Map接口描述,看到Map有多个子类,这里我们主要讲解常用的HashMap集合、LinkedHashMap集合。
- HashMap<K,V>:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
- LinkedHashMap<K,V>:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
tips:Map接口中的集合都有两个泛型变量<K,V>,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量<K,V>的数据类型可以相同,也可以不同。
Map集合常用方法
public V put(K key, V value)
: 把指定的键与指定的值添加到Map集合中。public V remove(Object key)
: 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。public V get(Object key)
根据指定的键,在Map集合中获取对应的值。boolean containsKey(Object key)
判断集合中是否包含指定的键。public Set<K> keySet()
: 获取Map集合中所有的键,存储到Set集合中。public Set<Map.Entry<K,V>> entrySet()
: 获取到Map集合中所有的键值对对象的集合(Set集合)。
Map集合遍历键找值方式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fToB9CP1-1660922686078)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\06.集合\15.【Map】-笔记\resource\01_Map集合遍历键找值方式.bmp)]
Entry键值对对象
我们已经知道,Map
中存放的是两种对象,一种称为key(键),一种称为value(值),它们在在Map
中是一一对应关系,这一对对象又称做Map
中的一个Entry(项)
。Entry
将键值对的对应关系封装成了对象。即键值对对象,这样我们在遍历Map
集合时,就可以从每一个键值对(Entry
)对象中获取对应的键与对应的值。
既然Entry表示了一对键和值,那么也同样提供了获取对应键和对应值得方法:
public K getKey()
:获取Entry对象中的键。public V getValue()
:获取Entry对象中的值。
在Map集合中也提供了获取所有Entry对象的方法:
public Set<Map.Entry<K,V>> entrySet()
: 获取到Map集合中所有的键值对对象的集合(Set集合)。
Map集合遍历键值对方式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YtvCtpvU-1660922686078)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\06.集合\15.【Map】-笔记\resource\02_Map集合遍历键值对方式.bmp)]
注意:Map集合不能直接使用迭代器或者foreach进行遍历。但是转成Set之后就可以使用了。
12.异常
异常概念
- 异常 :指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。
在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。
注意:异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.
-
异常的产生过程解析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oexlZRfj-1660922686079)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\16.【异常、线程】-笔记\resource\01_异常的产生过程解析.bmp)]
异常的分类
异常(Exception)的分类:根据在编译时期还是运行时期去检查异常?
- 编译时期异常:checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)
- 运行时期异常:runtime异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数学异常)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iBGeDfIx-1660922686079)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\16.【异常、线程】-笔记\就业班-day05-异常、线程\img\异常的分类.png)]
异常的处理
抛出异常
使用格式:
throw new 异常类名(参数);
例如:
throw new NullPointerException("要访问的arr数组不存在");
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");
注意:如果产生了问题,我们就会throw将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续讲问题声明出去,使用throws声明处理。
声明异常throws
声明异常:将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,而没有捕获处理(稍后讲解该方式),那么必须通过throws进行声明,让调用者去处理。
关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).
声明异常格式:
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{ }
捕获异常try…catch
如果异常出现的话,会立刻终止程序,所以我们得处理异常:
- 该方法不处理,而是声明抛出,由该方法的调用者来处理(throws)。
- 在方法中使用try-catch的语句块来处理异常。
try-catch的方式就是捕获异常。
- 捕获异常:Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理。
捕获异常语法如下:
try{
编写可能会出现异常的代码
}catch(异常类型 e){
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
**try:**该代码块中编写可能产生异常的代码。
**catch:**用来进行某种异常的捕获,实现对捕获到的异常进行处理。
自定义异常
- 自定义一个编译期异常: 自定义类 并继承于
java.lang.Exception
。 - 自定义一个运行时期的异常类:自定义类 并继承于
java.lang.RuntimeException
。
13.多线程
并行与并发
- 并发:指两个或多个事件在同一个时间段内发生。(一个CPU(采用时间片)同时执行多个任务—秒杀、多个人做同一件事)
- 并行:指两个或多个事件在同一时刻发生(同时发生)。(多个CPU同时执行多个任务—多个人同时做不同的事)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-exsbtwAe-1660922686079)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\16.【异常、线程】-笔记\就业班-day05-异常、线程\img\并行与并发.bmp)]
线程与进程
-
进程:是指一个内存中运行的应用程序,每个进程作为资源分配的单位,进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程(生命周期)。
-
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc)(register) 。
-
一个进程中的多个线程共享相同的内存单元/内存地址空间—>它们从同一堆中分配对象,可以 访问相同的变量和对象.这就使得线程间通信更简便、高效。但多个线程操作共享的系统资 源可能就会带来安全的隐患(需要用线程同步解决).
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
- 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc() 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ho2fFD18-1660922686080)(C:\Users\腾腾\AppData\Roaming\Typora\typora-user-images\image-20220203214954819.png)]
多线程的优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
何时需要多线程:
- 程序需要同时执行两个或多个任务
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写 操作、网络操作、搜索等
- 需要一些后台运行的程序时
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TlY0nnov-1660922686080)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\16.【异常、线程】-笔记\resource\04_线程概念.bmp)]
主线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nmkT8IQG-1660922686080)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\16.【异常、线程】-笔记\resource\05_主线程.bmp)]
线程调度
-
分时调度(时间片)
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-auxugPM4-1660922686081)(C:\Users\腾腾\AppData\Roaming\Typora\typora-user-images\image-20220203220618022.png)]
-
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
线程的优先级
- 线程的优先级等级
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5
- 涉及的方法
- getPriority() :返回线程优先值
- setPriority(int newPriority) :改变线程的优先级
- 说明
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PI7KqPfk-1660922686081)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\16.【异常、线程】-笔记\就业班-day05-异常、线程\img\设置线程优先级.bmp)]
创建线程类—继承Thread类
自定义线程类
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}
测试类
public class Demo {
public static void main(String[] args) {
System.out.println("这里是main线程");
MyThread mt = new MyThread("小强");
mt.start();//开启了一个新的线程
for (int i = 0; i < 20; i++) {
System.out.println("旺财:"+i);
}
}
}
//如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式
//一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SuibDgKN-1660922686081)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\17.【线程、同步】-笔记\resource\img\线程流程图.png)]
多线程原理
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6pOcuEB1-1660922686082)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\17.【线程、同步】-笔记\resource\img\栈内存原理图.bmp)]
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLy81MEH-1660922686082)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\17.【线程、同步】-笔记\resource\02_多线程内存图解.bmp)]
多线程随机打印结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WLPmVZOF-1660922686082)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\17.【线程、同步】-笔记\resource\01_多线程随机性打印结果.bmp)]
创建线程类—实现Runnable接口
public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
Runnable和Thread的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
扩充:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进 程。
使用匿名内部类方式实现线程的创建
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法:
public class NoNameInnerClassThread {
public static void main(String[] args) {
// new Runnable(){
// public void run(){
// for (int i = 0; i < 20; i++) {
// System.out.println("张宇:"+i);
// }
// }
// }; //‐‐‐这个整体 相当于new MyRunnable()
Runnable r = new Runnable(){
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("张宇:"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 20; i++) {
System.out.println("费玉清:"+i);
}
}
}
Thread类的有关方法
/**
* 测试Thread中的常用方法:
* 1. start():启动当前线程;调用当前线程的run()
* 2. run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
* 3. currentThread():静态方法,返回执行当前代码的线程
* 4. getName():获取当前线程的名字
* 5. setName():设置当前线程的名字
* 6. yield():释放当前cpu的执行权
* 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
* 若队列中没有同优先级的线程,忽略此方法
* 7. join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才
* 结束阻塞状态。 与yield的区别 yield还参与竞争 而join直接阻塞等待
* 8. stop():已过时。当执行此方法时,强制结束当前线程。
* 9. sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前
* 线程是阻塞状态。
* 10. isAlive():判断当前线程是否存活
* 线程的优先级:
* 1.
* MAX_PRIORITY:10
* MIN _PRIORITY:1
* NORM_PRIORITY:5 -->默认优先级
* 2.如何获取和设置当前线程的优先级:
* getPriority():获取线程的优先级
* setPriority(int p):设置线程的优先级
* 说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下
* 被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
*/
class HelloThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
// if (i % 20 == 0) {
// Thread.yield();
// }
}
}
public HelloThread(String name) {
super(name);
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
HelloThread h1 = new HelloThread("Thread:1");
// h1.setName("线程一");
//设置分线程的优先级
h1.setPriority(Thread.MAX_PRIORITY);
h1.start();
//给主线程命名
Thread.currentThread().setName("主线程");
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
if (i == 20) {
try {
//主线程中调用线程一的join方法 --> 导致主线程阻塞 直到线程一结束
h1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
线程的生命周期
JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时(new之后),新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yZV8TwcD-1660922686083)(C:\Users\腾腾\AppData\Roaming\Typora\typora-user-images\image-20220203221200583.png)]
线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样 的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
我们通过一个案例,演示线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个 (本场电影只能卖100张票)。
我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票) 需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hbfzo8u4-1660922686083)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\17.【线程、同步】-笔记\resource\03_线程安全问题的概述.bmp)]
模拟票:
public class Ticket implements Runnable{
private int ticket = 100;
/**
* 执行卖票操作
*/
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
}
测试类
public class Test {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
发现程序出现了两个问题:
- 相同的票数,比如5这张票被卖了两回。
-
- 不存在的票,比如0票与-1票,是不存在的。
这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。
线程安全问题产生原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vJLMYjrh-1660922686084)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\17.【线程、同步】-笔记\resource\04_线程安全问题产生的原理.bmp)]
线程同步
问题的提出
模拟火车站售票程序,开启三个窗口售票。(分别用继承和实现接口方式)
/**
* 例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式
* 存在线程的安全问题,待解决。
*/
class Window extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
/**
* 例子:创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式
* 存在线程的安全问题,待解决。
*/
class Window1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-40m3z7Iz-1660922686084)(C:\Users\腾腾\AppData\Roaming\Typora\typora-user-images\image-20220203221840751.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dYskI6JK-1660922686084)(C:\Users\腾腾\AppData\Roaming\Typora\typora-user-images\image-20220203221853981.png)]
- 多线程出现了安全问题
- 问题的原因: 当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
- 解决办法: 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制 (synchronized)来解决。 根据案例简述:
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
那么怎么去使用呢?有三种方式完成同步操作:
- 同步代码块
- 同步方法
- 锁机制
同步代码块:synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
分析同步的原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WUJiGKlh-1660922686085)(C:\Users\腾腾\AppData\Roaming\Typora\typora-user-images\image-20220203222151537.png)]
首先线程t1进入同步代码块,然后锁住;打印车票后,解锁,让其他线程进入同步代码块
同步锁中的锁
- 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)
- 同步方法的锁:静态方法(类名.class)、非静态方法(this)
- 同步代码块:自己指定,很多时候也是指定为this或类名.class
注意
- 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就 无法保证共享资源的安全
- 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着 (BLOCKED)。 使用同步代码块解决
同步的范围
- 如何找问题,即代码是否存在线程安全?(非常重要)
- 明确哪些代码是多线程运行的代码
- 明确多个线程是否有共享数据
- 明确多线程运行代码中是否有多条语句操作共享数据
- 如何解决呢?(非常重要)
- 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。 即所有操作共享数据的这些语句都要放在同步范围中
- 切记:
- 范围太小:没锁住所有有安全问题的代码
- 范围太大:没发挥多线程的功能
释放锁的操作
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、 该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的**wait()**方法,当前线程暂停,并释放锁。
不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、 Thread.yield()方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程 挂起,该线程不会释放锁(同步监视器)。
- 应尽量避免使用suspend()和resume()来控制线程
单例设计模式之懒汉式(线程安全)
/**
* 使用同步机制将单例模式中的懒汉式改写为线程安全的
*/
public class BankTest {
}
/**
* 懒汉式单例模式 有线程安全问题 解决
*/
class Bank {
private Bank() {}
private static Bank instance = null;
public static Bank getInstance() {
//效率高 后续线程不用进去同步代码块
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
//效率低
// synchronized (Bank.class) {
// if (instance == null) {
// instance = new Bank();
// }
// return instance;
// }
}
}
死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于 阻塞状态,无法继续
同步原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9GvaIrXZ-1660922686085)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\17.【线程、同步】-笔记\resource\05_同步的原理.bmp)]
使用同步代码块解决代码:
public class Ticket implements Runnable{
private int ticket = 100;
Object lock = new Object();
/**
* 执行卖票操作
*/
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticket > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
}
}
同步方法: 使用synchronized
修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外 等着。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
使用同步方法代码如下:
public class Ticket implements Runnable{
private int ticket = 100;
/**
* 执行卖票操作
*/
@Override
public void run() {
while (true) {
sellTicket();
}
}
public synchronized void sellTicket() {
if (ticket > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
Lock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
-
public void lock()
:加同步锁。 -
public void unlock()
:释放同步锁。
使用如下:
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
/**
* 执行卖票操作
*/
@Override
public void run() {
while (true) {
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
lock.unlock();
}
}
}
14.线程池与Lambda表达式
线程池
线程池: 其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
线程池工作原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KnDcbFwg-1660922686085)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\07.异常与多线程\18.【线程池、Lambda表达式】-笔记\就业班-day07-线程池、Lambda表达式\img\线程池原理.bmp)]
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池的使用
Java里面线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService
。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:
-
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不做)。
Runnable实现类代码:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教练回到了游泳池");
}
}
线程池测试类:
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
MyRunnable r = new MyRunnable();
//自己创建线程对象的方式
// Thread t = new Thread(r);
// t.start(); ---> 调用MyRunnable中的run()
// 从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
// 再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池
//service.shutdown();
}
}
Lambda表达式
函数式编程思想:只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程—强调做什么,而不是以什么形式做
冗余的Runnable代码
传统写法
当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable
接口来定义任务内容,并使用java.lang.Thread
类来启动该线程。代码如下:
public class Demo01Runnable {
public static void main(String[] args) {
// 匿名内部类
Runnable task = new Runnable() {
@Override
public void run() { // 覆盖重写抽象方法
System.out.println("多线程任务执行!");
}
};
new Thread(task).start(); // 启动线程
}
}
本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable
接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
代码分析
对于Runnable
的匿名内部类用法,可以分析出几点内容:
Thread
类需要Runnable
接口作为参数,其中的抽象run
方法是用来指定线程任务内容的核心;- 为了指定
run
的方法体,不得不需要Runnable
接口的实现类; - 为了省去定义一个
RunnableImpl
实现类的麻烦,不得不使用匿名内部类; - 必须覆盖重写抽象
run
方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错; - 而实际上,似乎只有方法体才是关键所在。
体验Lambda的更优写法
借助Java 8的全新语法,上述Runnable
接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:
public class Demo02LambdaRunnable {
public static void main(String[] args) {
new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
}
}
回顾匿名内部类
Lambda是怎样击败面向对象的?在上例中,核心代码其实只是如下所示的内容:
() -> System.out.println("多线程任务执行!")
为了理解Lambda的语义,我们需要从传统的代码起步。
使用实现类
要启动一个线程,需要创建一个Thread
类的对象并调用start
方法。而为了指定线程执行的内容,需要调用Thread
类的构造方法:
public Thread(Runnable target)
为了获取Runnable
接口的实现对象,可以为该接口定义一个实现类RunnableImpl
:
public class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println("多线程任务执行!");
}
}
然后创建该实现类的对象作为Thread
类的构造参数:
public class Demo03ThreadInitParam {
public static void main(String[] args) {
Runnable task = new RunnableImpl();
new Thread(task).start();
}
}
使用匿名内部类
这个RunnableImpl
类只是为了实现Runnable
接口而存在的,而且仅被使用了唯一一次,所以使用匿名内部类的语法即可省去该类的单独定义,即匿名内部类:
public class Demo04ThreadNameless {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多线程任务执行!");
}
}).start();
}
}
匿名内部类的好处与弊端
一方面,匿名内部类可以帮我们省去实现类的定义;另一方面,匿名内部类的语法——确实太复杂了!
语义分析
仔细分析该代码中的语义,Runnable
接口只有一个run
方法的定义:
public abstract void run();
即制定了一种做事情的方案(其实就是一个函数):
- 无参数:不需要任何条件即可执行该方案。
- 无返回值:该方案不产生任何结果。
- 代码块(方法体):该方案的具体执行步骤。
同样的语义体现在Lambda
语法中,要更加简单:
() -> System.out.println("多线程任务执行!")
- 前面的一对小括号即
run
方法的参数(无),代表不需要任何条件; - 中间的一个箭头代表将前面的参数传递给后面的代码;
- 后面的输出语句即业务逻辑代码。
Lambda表达式标准格式
Lambda省去面向对象的条条框框,格式由3个部分组成:
- 一些参数
- 一个箭头
- 一段代码
Lambda表达式的标准格式为:
(参数类型 参数名称) -> { 代码语句 }
格式说明:
- 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
->
是新引入的语法格式,代表指向动作。- 大括号内的语法与传统方法体要求基本一致。
Lambda省略规则
在Lambda标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略;
- 如果小括号内有且仅有一个参,则小括号可以省略;
- 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
Lambda表达式使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
无论是JDK内置的Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。 - 使用Lambda必须具有上下文推断。
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有一个抽象方法的接口,称为“函数式接口”。
15.File类
构造方法
public File(String pathname)
:通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。public File(String parent, String child)
:从父路径名字符串和子路径名字符串创建新的 File实例。public File(File parent, String child)
:从父抽象路径名和子路径名字符串创建新的 File实例。
常用方法
获取功能方法
-
public String getAbsolutePath()
:返回此File的绝对路径名字符串。 -
public String getPath()
:将此File转换为路径名字符串。 -
public String getName()
:返回由此File表示的文件或目录的名称。 -
public long length()
:返回由此File表示的文件的长度。
绝对路径和相对路径
- 绝对路径:从盘符开始的路径,这是一个完整的路径。
- 相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用
判断功能方法
public boolean exists()
:此File表示的文件或目录是否实际存在。public boolean isDirectory()
:此File表示的是否为目录。public boolean isFile()
:此File表示的是否为文件。
创建和删除功能方法
public boolean createNewFile()
:当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。public boolean delete()
:删除由此File表示的文件或目录。public boolean mkdir()
:创建由此File表示的目录。public boolean mkdirs()
:创建由此File表示的目录,包括任何必需但不存在的父目录。
目录的遍历
-
public String[] list()
:返回一个String数组,表示该File目录中的所有子文件或目录。 -
public File[] listFiles()
:返回一个File数组,表示该File目录中的所有的子文件或目录。
文件过滤器
java.io.FileFilter
是一个接口,是File的过滤器。 该接口的对象可以传递给File类的listFiles(FileFilter)
作为参数, **接口中只有一个方法。**可以使用匿名内部类或者Lambda表达式。
boolean accept(File pathname)
:测试pathname是否应该包含在当前File目录中,符合则返回true。
FileFilter过滤器原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YDs15Uk5-1660922686086)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\08.File类与IO流\19.【File类、递归】-笔记\resource\04_FileFilter过滤器的原理.bmp)]
16.字节流与字符流
IO的概述
IO的分类
根据数据的流向分为:输入流和输出流。
- 输入流 :把数据从
其他设备
上读取到内存
中的流。 - 输出流 :把数据从
内存
中写出到其他设备
上的流。
格局数据的类型分为:字节流和字符流。
- 字节流 :以字节为单位,读写数据的流。
- 字符流 :以字符为单位,读写数据的流。
IO的流向说明
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PwMSD1Ds-1660922686086)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\08.File类与IO流\20.【字节流、字符流】-笔记\就业班-day09-字节流、字符流\img\1_io.jpg)]
顶级的父类们
输入流(读取文件) | 输出流(写入文件) | |
---|---|---|
字节流 | 字节输入流 InputStream | 字节输出流 OutputStream |
字符流 | 字符输入流 Reader | 字符输出流 Writer |
字节流
字节输出流【OutputStream】
java.io.OutputStream
抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。
public void close()
:关闭此输出流并释放与此流相关联的任何系统资源。public void flush()
:刷新此输出流并强制任何缓冲的输出字节被写出。public void write(byte[] b)
:将 b.length字节从指定的字节数组写入此输出流。public void write(byte[] b, int off, int len)
:从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。public abstract void write(int b)
:将指定的字节输出流。
小贴士:
close方法,当完成流的操作时,必须调用此方法,释放系统资源。
FileOutputStream类
OutputStream
有很多子类,我们从最简单的一个子类开始。
java.io.FileOutputStream
类是文件输出流,用于将数据写出到文件。
构造方法
public FileOutputStream(File file)
:创建文件输出流以写入由指定的 File对象表示的文件。public FileOutputStream(String name)
: 创建文件输出流以指定的名称写入文件。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。
数据追加续写
经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?
public FileOutputStream(File file, boolean append)
: 创建文件输出流以写入由指定的 File对象表示的文件。public FileOutputStream(String name, boolean append)
: 创建文件输出流以指定的名称写入文件。
这两个构造方法,参数中都需要传入一个boolean类型的值,true
表示追加数据,false
表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了
写出换行
Windows系统里,换行符号是\r\n
系统中的换行:
- Windows系统里,每行结尾是
回车+换行
,即\r\n
; - Unix系统里,每行结尾只有
换行
,即\n
; - Mac系统里,每行结尾是
回车
,即\r
。从 Mac OS X开始与Linux统一。
字节输入流【InputStream】
java.io.InputStream
抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
public void close()
:关闭此输入流并释放与此流相关联的任何系统资源。public abstract int read()
: 从输入流读取数据的下一个字节。public int read(byte[] b)
: 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。
小贴士:
close方法,当完成流的操作时,必须调用此方法,释放系统资源。
FileInputStream类
java.io.FileInputStream
类是文件输入流,从文件中读取字节。
构造方法
FileInputStream(File file)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。FileInputStream(String name)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出FileNotFoundException
。
字节流读取文件的原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iL2EHx34-1660922686087)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\08.File类与IO流\20.【字节流、字符流】-笔记\resource\03_字节流读取文件的原理.bmp)]
文件复制的原理 硬盘–>内存–>硬盘
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RcsHaTVZ-1660922686087)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\08.File类与IO流\20.【字节流、字符流】-笔记\resource\04_文件复制的原理.bmp)]
字符流
当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。
字符输入流【Reader】
java.io.Reader
抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。
public void close()
:关闭此流并释放与此流相关联的任何系统资源。public int read()
: 从输入流读取一个字符。public int read(char[] cbuf)
: 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。
FileReader类
java.io.FileReader
类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
小贴士:
-
字符编码:字节与字符的对应规则。Windows系统的中文编码默认是GBK编码表。
idea中UTF-8
-
字节缓冲区:一个字节数组,用来临时存储字节数据。
构造方法
FileReader(File file)
: 创建一个新的 FileReader ,给定要读取的File对象。FileReader(String fileName)
: 创建一个新的 FileReader ,给定要读取的文件的名称。
当你创建一个流对象时,必须传入一个文件路径。类似于FileInputStream 。
字符输出流【Writer】
java.io.Writer
抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。
void write(int c)
写入单个字符。void write(char[] cbuf)
写入字符数组。abstract void write(char[] cbuf, int off, int len)
写入字符数组的某一部分,off数组的开始索引,len写的字符个数。void write(String str)
写入字符串。void write(String str, int off, int len)
写入字符串的某一部分,off字符串的开始索引,len写的字符个数。void flush()
刷新该流的缓冲。void close()
关闭此流,但要先刷新它。
FileWriter类
java.io.FileWriter
类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
构造方法
FileWriter(File file)
: 创建一个新的 FileWriter,给定要读取的File对象。FileWriter(String fileName)
: 创建一个新的 FileWriter,给定要读取的文件的名称。
当你创建一个流对象时,必须传入一个文件路径,类似于FileOutputStream。
关闭和刷新
因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush
方法了。
flush
:刷新缓冲区,流对象可以继续使用。close
:先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
小贴士:字符流,只能操作文本文件,不能操作图片,视频等非文本文件。
当我们单纯读或者写文本文件时 使用字符流 其他情况使用字节流
属性集
概述
java.util.Properties
继承于 Hashtable
,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时,System.getProperties
方法就是返回一个Properties
对象。
Properties类
构造方法
public Properties()
:创建一个空的属性列表。
基本的存储方法
public Object setProperty(String key, String value)
: 保存一对属性。public String getProperty(String key)
:使用此属性列表中指定的键搜索属性值。public Set<String> stringPropertyNames()
:所有键的名称的集合。
与流相关的方法
public void load(InputStream inStream)
: 从字节输入流中读取键值对。
参数中使用了字节输入流,通过流对象,可以关联到某文件上,这样就能够加载文本中的数据了。文本数据格式:
filename=a.txt
length=209385038
location=D:\a.txt
17.缓冲流
概述
缓冲流,也叫高效流,是对4个基本的FileXxx
流的增强,所以也是4个流,按照数据类型分类:
- 字节缓冲流:
BufferedInputStream
,BufferedOutputStream
- 字符缓冲流:
BufferedReader
,BufferedWriter
缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。
缓冲流的原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GpaCU7ms-1660922686088)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\08.File类与IO流\21.【缓冲流、转换流、序列化流、打印流】-笔记\resource\01_缓冲流的原理.bmp)]
字节缓冲流
构造方法
public BufferedInputStream(InputStream in)
:创建一个 新的缓冲输入流。public BufferedOutputStream(OutputStream out)
: 创建一个新的缓冲输出流。
字符缓冲流
构造方法
public BufferedReader(Reader in)
:创建一个 新的缓冲输入流。public BufferedWriter(Writer out)
: 创建一个新的缓冲输出流。
特有方法
字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。
- BufferedReader:
public String readLine()
: 读一行文字。 - BufferedWriter:
public void newLine()
: 写一行行分隔符,由系统属性定义符号。
readLine
方法演示,代码如下:
public class BufferedReaderDemo {
public static void main(String[] args) throws IOException {
// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("in.txt"));
// 定义字符串,保存读取的一行文字
String line = null;
// 循环读取,读取到最后返回null
while ((line = br.readLine())!=null) {
System.out.print(line);
System.out.println("------");
}
// 释放资源
br.close();
}
}
newLine
方法演示,代码如下:
public class BufferedWriterDemo throws IOException {
public static void main(String[] args) throws IOException {
// 创建流对象
BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));
// 写出数据
bw.write("黑马");
// 写出换行
bw.newLine();
bw.write("程序");
bw.newLine();
bw.write("员");
bw.newLine();
// 释放资源
bw.close();
}
}
输出效果:
黑马
程序
员
18.网络编程
ip与端口号
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MNjuCt3Y-1660922686088)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\09.网络编程\22.【网络编程】-笔记\resource\01_端口号.bmp)]
Socket类
Socket
类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。
构造方法
public Socket(String host, int port)
:创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为回送地址。
成员方法
public InputStream getInputStream()
: 返回此套接字的输入流。- 如果此Scoket具有相关联的通道,则生成的InputStream 的所有操作也关联该通道。
- 关闭生成的InputStream也将关闭相关的Socket。
public OutputStream getOutputStream()
: 返回此套接字的输出流。- 如果此Scoket具有相关联的通道,则生成的OutputStream 的所有操作也关联该通道。
- 关闭生成的OutputStream也将关闭相关的Socket。
public void close()
:关闭此套接字。- 一旦一个socket被关闭,它不可再使用。
- 关闭此socket也将关闭相关的InputStream和OutputStream 。
public void shutdownOutput()
: 禁用此套接字的输出流。- 任何先前写出的数据将被发送,随后终止输出流。
ServerSocket类
ServerSocket
类:这个类实现了服务器套接字,该对象等待通过网络的请求。
构造方法
public ServerSocket(int port)
:使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。
成员方法
public Socket accept()
:侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。
简单的TCP网络程序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VcMvZyAe-1660922686089)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\09.网络编程\22.【网络编程】-笔记\resource\02_TCP通信的概述.bmp)]
TCP通信分析图解
- 【服务端】启动,创建ServerSocket对象,等待连接。
- 【客户端】启动,创建Socket对象,请求连接。
- 【服务端】接收连接,调用accept方法,并返回一个Socket对象。
- 【客户端】Socket对象,获取OutputStream,向服务端写出数据。
- 【服务端】Scoket对象,获取InputStream,读取客户端发送的数据。
到此,客户端向服务端发送数据成功。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cPiZHA7b-1660922686089)(D:\java学习资料\黑马java讲义\阶段01.javase基础+高级\09.网络编程\22.【网络编程】-笔记\就业班-day11-网络编程\img\5_简单通信.jpg)]
自此,服务端向客户端回写数据。
- 【服务端】Socket对象,获取OutputStream,向客户端回写数据。
- 【客户端】Scoket对象,获取InputStream,解析回写数据。
- 【客户端】释放资源,断开连接。