目录
1.别再问为什么在类里面写个sysout语句爆红了,类里面有且只有五个成分:
2.面向对象三大特征: 封装,继承,多态 (扫盲扫盲,别这个都不知道)
9.枚举类(枚举类相当于多例模式)因为这个类比较抽象,少见,我多贴一点代码
1.别再问为什么在类里面写个sysout语句爆红了,类里面有且只有五个成分:
①成员变量(Filed:描述类或对象的属性信息
②成员方法(Method:描述类或对象的行为信息
③构造器(Constructor:初始化一个类的对象并返回引用
//构造器特点:
1.和类名相同。
2.没有返回值。
作用:
1.new的本质是在调用构造器,默认有一个无参构造器
2.初始化对象的值
④代码块(根据有无static分为静态代码块和实例代码块)
ps:实例代码块用的比较少,格式:{ },实例代码块属于对象,会跟对象一起自动加载。
static{
.....//静态代码属于类,会与类一起加载,自动执行一次.
}
⑤内部类(自己不怎么会写):内部类可以提供更好的封装性,因为外部类只能用public修饰,内部类有更多的修饰符,可以用private。
//实例内部类里面不能定义静态成员,因为实例内部类属于外部对象(但是可以定义常量,static final),静态内部类宿主是外部类本身,其实还是得记得静态static是属于类的,实例是属于对象的就好。
当然还有一个比较常用且重要的匿名内部类,会拿出单独讲解一下。
除了这五个成分,其他直接放在类下就会报错!!!
2.面向对象三大特征: 封装,继承,多态 (扫盲扫盲,别这个都不知道)
什么是封装呢,简单来说就是用private,public等修饰词来合理暴露,合理隐藏对象和数据,封装可以提高安全性,可以实现代码的组件化。
继承:继承可以提高代码的复用,子类不能继承父类的构造器(子类有自己的构造器)。子类构造器第一行默认有一个super();调用父类无参构造器,一直存在!
多态:可以实现类与类之间的解耦 父类 对象名=new 子类
//如需调用子类的特有方法,则不能使用多态形式创建子类对象
3.讲一下static这个很重要的关键词
首先我们要清楚:
-
static修饰的成员变量和方法,从属于类
-
普通变量和方法从属于对象
-
静态方法不能调用非静态成员,编译会报错 //面试可能会问哟
显然,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。
这里分享一个讲解static关键词比较明了的文章:http://t.csdn.cn/S9ZkS
4.this和super关键词:
this代表了当前对象的引用(继承中指代了子类对象):
this.子类成员变量;
this.子类成员方法;
/* this(....)可以根据参数访问本类其他构造器
应用场景:
Class person{
private String name;
private int age;
public person(String name,String age){
this.name=name;
this.age=age
};
public person(String name){
//借用其他构造器
this(name,18);//实现需求:不写年龄时,默认为18
}
}
super代表了当前对象的引用(继承中指代了父类对象):
super.父类成员变量;
super.父类成员方法;
super(....)可以根据参数访问父类其他构造器
5.深入讲解一下abstract(抽象)
抽象类是为了被子类继承,抽象类体现了模板思想,即:抽象类中已经实现的是模板中确定的成员,抽象类不确定如何实现的定义成抽象方法,交给具体的子类去实现。
一个类继承了抽象类,必须重写他所有的抽象方法,否则这个类也必须定义为抽象类。(当然这个只是语法上的意义,一般不会这么做)
抽象类的特征:有得有失
有得:得到了拥有抽象方法的能力。
有失:失去了创建对象的能力(抽象类不能创建对象)。
经典面试题:抽象类是否有构造器,是否能创建对象?
答:抽象类一定有构造器,抽象是被继承的,抽象类虽然有构造器但是不能创建对象(抽象类的抽象方法没有具体的方法体,创建对象没有意义)
抽象类有一个很重要的作用:
抽象类可以实现接口!
当一个类需要去实现一个接口时,如果该类实现了接口中的所有方法,则该类既可以定义为实体类也可以定义为抽象类;
如果该类实现了接口中的部分方法,还有部分方法没有实现,没有实现的部分方法只能继续以抽象方法的形式存在该类中,则该类必须定义为抽象类。
这么做的目的是:当我们需要定义一个类去实现接口中的部分方法时,我们可以先通过抽象类来实现其它部分的方法,然后去继承这个抽象类,就可以达到只实现接口中部分方法的目的;
试想如果是需要定义多个类都需要去实现接口中的部分方法,这时抽象类的作用就突出了,可以降低实现类实现接口的难度,同时解决了代码冗余的问题,所以这种情况在实际开发中的应用场景也是很多的。
6.深入理解一下接口(说完抽象类,就要说到接口了)
抽象类和接口有什么区别:
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的
接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
分享一篇讲区别比较好理解的文章:深入理解Java的接口和抽象类 - Matrix海子 - 博客园
JDK1.8以后接口新增了三个方法(默认方法,静态方法,私有方法):自己百度吧,不怎么常用。
接口和抽象类的小结:接口和抽象类都有抽象方法,都不能创建对象,但是抽象类还能有普通成员变量,静态的,构造器,但抽象类只能被单继承,接口能被多实现。
7.final关键字
final可以用于修饰类、方法、对象
(1)final修饰类:类不能被继承
(2)final修饰方法:方法不能被重写
(3)final修饰变量:变量有且只能被复制一次
拓展:final修饰静态成员变量时,为常量 public static final。
给final定义的变量赋值要么在定义时候赋值,要么在静态代码块赋值。
拓展:abstract和final是互斥关系,例如类一个是为了被继承,一个不能被继承
8.单例模式(了解概念,具体去看设计模式吧)
单例的意思上一个类永远只能有一个对象,不能创建多个对象。
为什么要用单例?
对象越多越占内存,有时候只需要一个对象就能实现业务,单例可以节约内存,提高性能。
饿汉式单例,懒汉式单例:单例模式懒汉式和饿汉式区别_AH_HH的博客-CSDN博客_懒汉式和饿汉式
9.枚举类(枚举类相当于多例模式)因为这个类比较抽象,少见,我多贴一点代码
枚举类作用:
枚举类可以用于做信息标志和信息分类。
枚举类的特点:
①.枚举类是final修饰的,不能被继承
②.枚举类构造器私有,不能创建对象
③所有枚举值都是public static final的。注意这一点只是针对于枚举值,我们可以和在普通类里面定义 变量一样定义其它任何类型的非枚举变量,这些变量可以用任何你想用的修饰符
④.Enum默认实现了java.lang.Comparable接口。
⑤.Enum覆载了了toString方法,因此我们如果调用Color.Blue.toString()默认返回字符串”Blue”.
⑥.Enum提供了一个valueOf方法,这个方法和toString方法是相对应的。调用valueOf(“Blue”)将返回Color.Blue.因此我们在自己重写toString方法的时候就要注意到这一点,一把来说应该相对应地重写valueOf方法。
⑦.Enum还提供了values(),这个方法使你能够方便的遍历所有的枚举值。
⑧.Enum还有一个oridinal(),返回枚举对象的索引位置。
枚举类格式:
修饰符 enum 枚举名称{
实例1名称,实力2名称… ;
}
实例代码:
package com.tian.test;
/**
* 常量做信息标志和分类,虽然也挺好,但是入参不受控制,入参太随性无法严谨
* 枚举类用于做信息标志和信息分类:优雅!
*/
public enum Oritation {
UP,DOWN,LEFT,RIGHT;
public static void main(String[] args) {
move(Oritation.UP);
}
public static void move(Oritation o) {
switch(o) {
case UP:
System.out.println("控制玛丽向上跳");
break;
case DOWN:
System.out.println("控制玛丽向下蹲");
break;
case LEFT:
System.out.println("控制玛丽向左走");
break;
case RIGHT:
System.out.println("控制玛丽向右走");
break;
}
}
}
10.匿名内部类
什么是匿名内部类?
匿名内部类就是没有名字的内部类。
匿名内部类的作用?
简化代码。
什么时候用匿名内部类?
假如只创建某类的一个对象时,就不必将该类进行命名。匿名内部类的前提是存在一个类或者接口,且匿名内部类是写在方法中的。只针对重写一个方法时使用,需要重写多个方法时不建议使用。
匿名内部类格式:
new 类名(参数) | 实现接口()
{
// 匿名内部类的类体部分
}
从上面的定义可以看出,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或者实现一个接口。
两个规则:
匿名内部类不能是抽象类。
匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以初始化块,可以通过初始化块来完成构造器需要完成的工作。
匿名内部类的主要使用场景其实还是在监听器,线程创建之类的,当然匿名内部类还有一种简便写法,lambda表达式,当然这是后话了。
11. ==和equals方法
参考:java面试题之equals和==的区别_好想去买菜的博客-CSDN博客
个人总结:==是比较地址值,equals会先比较地址值,如果地址值不同再去比较数据。
一个大佬对于equals的解释:equals底层实现其实就是==,数据比较时分为两种情况,1:数据是基本类型,那么直接比较字面值即可,字面值相同就相等.2:数据是引用类型,那么就比较地址值.String是引用类型,在创建String时,两种方法,一种是直接赋值,这个时候,会把值存到常量池中,不会重新分配地址,所以不管是==还是equals都是true.第二种用new的方式,每次new都会分配一个新的地址,所以用==比较就是false,而String底层重写了equals和hashCode方法,只需要比较具体值是否相同,所以结果是true
12.包装类
Java认为一切皆对象,但八大基本类型却不是对象!
包装类的特殊作用:
①包装类作为类首先拥有了Object类的方法
②包装类作为引用类型的变量可以存储null值
例 int i=null ;//报错
Integer i=null;//不报错
基本数据和包装类之间的转换
自动装箱:基本数据类型转换为包装类;
自动拆箱:包装类转换为基本数据类型。
基本数据类型和包装类的转换
通过包装类Integer.toString()将整型转换为字符串;
通过Integer.parseInt()将字符串转换为int类型;
通过valueOf()方法把字符串转换为包装类然后通过自动拆箱。
13.正则表达式
正则表达式是由一些具有特殊含义的字符组成的字符串,多用于查找、替换符合规则的字符串。在表单验证、Url映射等处都会经常用到。
API文档查找pattern也可以。
最全常用正则表达式大全_ZhaoYingChao88的博客-CSDN博客_最全的常用正则表达式大全
14.泛型
首先要知道泛型就是一个标签: <数据类型>
泛型可以在编译阶段约束只能操作某种类型的数据,不会出现类型转换异常。
JDK1.7以后,泛型后面的申明可以不写。例:List<String> tempList = new ArrayList<>();
泛型和集合都只能引用数据类型,不支持基本数据类型(但是可以引用包装类)。
当然,泛型变量也可以自定义,一般用E,T, K, V
泛型接口:
泛型接口格式:
修饰符 interface 接口名称<泛性变量>{
}
泛型接口的核心思想:在实现接口的时候传入真实的数据类型,重写方法时就是对该数据类型操作
场景:定义一个泛型自定义接口
publi interface Data<E>{
void add(E stu);
void delete(E stu);
E query(int id); }
//创建一个学生类来调用接口
public class StudentData implemnts Data<Student>{ //告诉接口 E 是 Student 老师类异同
@Overidd
public void add(Student stud){
}
@Overidd
public void delete(Student stud){
}
@Overidd
public Student add(int id){
return null; }
注意;在其他类调用时,比较规范的写法是: Data<Student> data=new StudentData(); //多态
另外,泛型没有继承关系!
例:Student 和 Teacher都继承了Data,但是List<Student>,List<Teacher>和List<Data>无关系
因此,就出现了通配符:? //用List<?> 可以接纳List<Student>和List<Teacher>了
?可以在使用泛型时表示一切类型
而 E T K V是在定义泛型时表示一切类型
但是,通配符 ?会把一切类型能接纳进来,会出现错误,所以又出现了
泛型的上下限:
? extends Data:?必须是Data或者其子类(泛型的上限) // List<? extends Data>
? super Data: ?必须是Data或者其父类(泛型的下限,不常用)
15.Collection集合
深入理解Collection集合_JagTom的博客-CSDN博客
16.Collections(集合工具类)
什么是Collections?
Collections是针对Collection集合类的一个工具类
Collections常用的API
public static <T> boolean addAll(Collection<T> c, T.....elements):往集合中一次性添加一些元素。
public static void shuffle(List<?> list):打乱List集合顺序。
public static <T> void sort(List<T> list)::给List集合升序排序。
public static <T> void sort(List<T> list, Comparator<? super T>):根据指定比较器产生的顺序对指定列表进行排序。
package Collections;
import test.Student;
import java.util.*;
public class CollectionsDemo {
public static void main(String[] args) {
//public static <T> boolean addAll(Collection<T> c, T.....elements):往集合中一次性添加一些元素。
Collection<String> names = new ArrayList<>();
Collections.addAll(names,"小王","小赵","小李"); //[小王, 小赵, 小李]
System.out.println(names);
//public static void shuffle(List<?> list):打乱集合顺序。
List<String> list=new ArrayList<>();
Collections.addAll(list,"小王","小赵","小李");
Collections.shuffle(list);
System.out.println(list); //[小王, 小李, 小赵] //注意只能打乱List集合,因为只有List集合是有序的呀
//public static <T> void sort(List<T> list):给List集合升序排序。
Collections.sort(list);
System.out.println(list); //[小李, 小王, 小赵]
//public static <T> void sort(List<T> list, Comparator<? super T>):根据指定比较器产生的顺序对指定列表进行排序。
//和Treeset使用的比较器方法差不多,也可以对类直接写比较器 同样当两边都定义比较器时,采用就近原则
List<Student> set3 = new ArrayList<>();
Collections.sort(set3,new Comparator<Student>() {
@Override
public int compare(test.Student o1, test.Student o2) {
//o1比较者,o2被比较者
return o1.getStunage()-o2.getStunage();
}
});
set3.add(new test.Student(1, "小明"));
set3.add(new test.Student(2, "小王"));
set3.add(new Student(3, "小栗子"));
System.out.println(set3); //[Student{stunage=1, stuname='小明'}, Student{stunage=2, stuname='小王'}, Student{stunage=3, stuname='小栗子'}]
}
}
17.可变参数
可变参数在形参中可以接受多个数据
可变参数格式:数据类型...参数名称
可变参数只能有一个,只能放在最后(思考逻辑,因为它长度不定,会冲突)
import java.util.*;
public class test {
public static void main(String[] args) {
num(); //可以不传参数
num(11); //可以传一个
num(122,45,999); //可以传多个
num(new int[]{12,33,41});//可以传数组
}
public static void num(int...nums){
System.out.println(Arrays.toString(nums)); //其本质是个数组
//[]
//[11]
//[122, 45, 999]
// [12, 33, 41]
}
}
18.深入理解Map集合
Map与Collection在集合框架中属并列存在
Java Map集合的详解_王俊凯夫人的博客-CSDN博客_map集合
19.排序
冒泡排序
冒泡排序的作用:
可以用于对数组或集合的元素大小进行排序!
冒泡排序的核心算法思想:
int[ ] arr=new int[ ] {55,22,33,44};
从数组首部开始两两比较,较大的往后移,最后把最大的元素移到末尾。
冒泡排序的实现核心:
1.首先要确定冒泡几轮,轮数为数组长度-1;
2.每轮要两两比较几次,
i(轮数) 比较次数 每轮比较次数:数组长度-i-1
0 3
1 2
2 1
public class SelectSort {
public static void main(String[] args) {
int []nums=new int[]{55,66,11,22,99};
int length=nums.length; int swap;
//定义一个循环控制冒泡轮数 数组长度-1
for(int i=0;i<length-1;i++){
//再定义个循环每轮比较几次
for(int j=0;j<length-1-i;j++){
if(nums[j]>nums[j+1]){
swap=nums[j+1];
nums[j+1]=nums[j];
nums[j]=swap;
}
}
}
System.out.println(Arrays.toString(nums)); //[11, 22, 55, 66, 99]
}
}
选择排序
选择排序的思想:
从当前位置开始与后面每个元素比较,与较小值交换,最终把最小值交换到最前
选择排序的实现核心:
1.首先要确定选择几轮:轮数=数组长度-1;
2.每轮从当前位置比较几次
i(轮数) 比较次数 每轮比较次数:数组长度-i-1
0 3
1 2
2 1
public class SelectSort {
public static void main(String[] args) {
int []nums=new int[]{55,66,11,22,99};
int length=nums.length; int swap;
//定义一个循环控制选择几次
for(int i=0;i<length;i++){
//再定义一个循环来比较,一般是当前位置与后面元素比较
for(int j=i+1;j<length;j++){
if(nums[j]<nums[i]){ //把较小值交换
swap=nums[i];
nums[i]=nums[j];
nums[j]=swap;
}
}
}
System.out.println(Arrays.toString(nums)); //[11, 22, 55, 66, 99]
}
}
20.二分查找(折半查找)
二分查找:二分查找的前提是有序的。
二分查找的思路:每次查找时通过将待查找区间分成两部分并只取一部分继续查找,效率高
对于一个长度为 O(n) 的数组,二分查找的时间复杂度为 O(log n)
public class BinarySearch {
public static void main(String[] args) {
int arr[]=new int[]{33,22,11,55,66,77,88,99,100};
int search=binarysearch(arr,55);
System.out.println(search);
}
public static int binarysearch(int arr[],int number){
int start=0;
int end=arr.length-1;
//定义一个循环折中查找
while ((start<=end)){
//定位出中间元素的索引
int middleIndex = (start+end) /2;
// 拿当前元素与中间元素比较
if(number>arr[middleIndex]){
//当前元素大于中间元素,往右找,起始位置要改变
start = middleIndex+1;
}else if(number<arr[middleIndex]){
//当前元素小于中间元素,往右找,末尾位置要改变
end=middleIndex-1;
}else if(number==arr[middleIndex]){
return middleIndex;
}
}
return -1;
}
}
21.异常
22.深入理解多线程(并发知识点)
1.1进程和线程
什么是进程:
程序是静止的,运行中的程序就是进程。
进程的三个特性:
1.动态性:进程是运行中的程序,要动态的占用内存,cpu,网络等资源。
2.独立性:进程与进程之间是相互独立的,彼此有自己的独立内存区域。
3.并发性:假如cpu是单核的,同一时刻内存中只能存在一个进程被执行,cpu会分时轮询切换依次为每个进程服务,因为切换速度非常快,所以我们感觉这些进程在同时执行,这就是并发性。
并行:同一时刻有多个进程在同时执行。如果是在只有一个CPU的情况下,是无法实现并行的,因为同一时刻只能有一个进程被调度执行,如果此时同时要执行其他进程则必须上下文切换,这种只能称之为并发,而如果是多个CPU的情况下,就可以同时调度多个进程,这种就可以称之为并行。
什么是线程?
线程是属于进程的,一个线程只能有一个进程,一个进程可以有多个线程。
线程是进程中的一个独立执行单元。
线程创建开销相对于进程更小。
线程也支持并发性。
线程的作用?
可以提高程序的效率,因为线程也支持并发性,可以有更多机会得到cpu。
多线程可以解决很多业务模型。
大型高并发技术的技术核心。
小结:
线程类是继承了Thread的类
线程启动必须调用start()方法
多线程是并发抢占cpu的,执行过程中会出现随机性
1.2创建线程的三种方式
(1)直接定义一个类继承线程类Thread,重写run()方法,创建线程对象,调用线程对象的start()方法启动线程。(高耦合,不怎么用)
//优点:代码简单
//缺点:java是单继承,线程类继承了Thread类无法再继承其他类,功能受限,
//一些注意事项:
//start()方法底层是cpu注册了当前线程并且触犯了run()方法执行。
//而t.run()只是普通的调用了它的run方法,没有当成线程类来执行,没有添加线程
//建议线程先创建子线程,主线程任务放在之后。
public class Thread01 {
//启动后的Thread01就是一个进程
//main方法就是主线程
public static void main(String[] args) {
//3.创建一个线程对象
Thread t = new Mythread();
//4.启动线程
t.start();
for(int i=0;i<10;i++)
System.out.println("主线程"+i);
//匿名方式创建线程
new Mythread(){
@Override
public void run() {
for(int i=0;i<10;i++)
System.out.println("匿名子线程"+i);
}
}.start();
}
}
//1.直接定义一个类继承线程类Threa
class Mythread extends Thread{
//2.重写run()方法
@Override
public void run() {
for(int i=0;i<10;i++)
System.out.println("子线程"+i);
}
}
(2)声明实现 Runnable
接口的类。该类然后重写 run() 方法,创建实现类对象,把实现类对包装成线程对象,调用线程对象的start()方法开启线程。(常用)
//优点:线程类只是实现了个接口,还可以去继承其他类,实现更多的功能
//同一个线程任务对象可以被包装成多个各线程对象
//适合多个相同的程序代码的线程去共享同一个资源
//耦合性低,线程任务代码可以被多个线程共享,线程任务代码和线程独立
//线程池可以放入Runnable或Callable线程任务对象
注意:其实Thread类本身也是实现Runnable接口的
public class Thread02 {
public static void main(String[] args) {
//3.创建一个线程任务对象
Runnable target=new MyRunnable();
//4.把线程任务对象包装成线程对象
Thread t=new Thread(target,"1号线程");
Thread d=new Thread(target,"2号线程");
//5.调用start方法启动线程
t.start();;
d.start();
}
}
//1.创建一个线程任务类实现Runnable()接口
class MyRunnable implements Runnable{
//2.重写run()方法
@Override
public void run() {
for(int i=0;i<5;i++)
System.out.println(Thread.currentThread().getName()+"===>"+i);
}
}
匿名内部类写法:
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
(3)实现Callable接口.声明实现 Callable 接口的类。该类然后重写call() 方法,这个方法可以直接返回执行结果,创建实现类对象,把实现类对包装成未来任务对象,再把未来任务对象包装成线程对象。调用线程对象的start()方法开启线程。
//该方法有第二种方法的所有优点,此外还能得到返回结果,付出的代价就是要多一些代码。
public class Thread03 {
public static void main(String[] args) {
//3.创建一个Callable线程任务对象
Callable<String> cal=new CallableThread();
//4.把Callable任务对象封装成未来任务对象
FutureTask<String> futureTask=new FutureTask<>(cal);
//5.把未来任务对象包装成线程对象
// 未来任务是Runnable的实现类,未来任务对象其实就是一个Runnable对象
//未来任务对象可以在线程执行完毕后得到线程的返回结果
Thread thread=new Thread(futureTask);
//6.启动线程
thread.start();
String s= null;
try {
s = futureTask.get(); //得到返回结果
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(s);
}
}
//1.创建线程任务类实现Callable接口
class CallableThread implements Callable<String>{
//2.重写call方法
@Override
public String call() throws Exception {
int sum=0;
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "==>" + i);
sum+=i;
}
return Thread.currentThread().getName() + "执行结果==>" + sum;
}
}
Thread-0==>0
Thread-0==>1
Thread-0==>2
Thread-0==>3
Thread-0==>4
Thread-0==>5
Thread-0==>6
Thread-0==>7
Thread-0==>8
Thread-0==>9
Thread-0执行结果==>45
Process finished with exit code 0
1.3 Thread类的重要API
public final String getName() 返回该线程的名称。
public final void setName(String name) 改变线程名称为name。
public static Thread currentThread() 返回对当前正在执行的线程对象的引用,一般用来得到主线程
public static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
public class Thread01 {
public static void main(String[] args) {
Thread t = new Mythread();
t.start();
t.setName("1号线程");
Thread m=Thread.currentThread();
m.setName("我是主线程");
String s=m.getName();
for(int i=0;i<10;i++)
System.out.println(s+"===>"+i);
}}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "====>" + i);
}
}
}
Thread构造器:
--public Thread(){ }
--public Thread(String name){ }
--public Thread(Runnable target){ }
--public Thread(Runnable tartget,String name){ }
1.4线程同步
多个线程操作同一共享资源时可能会出现线程安全问题。
线程同步的作用:是为了解决线程安全问题。
线程同步的核心思想:让多个线程实现先后依次访问共享资源
线程同步的做法: 加锁
是把共享资源进行加锁,每次只能一个线程进来访问共享资源完毕后其他线程才能进来
线程同步的三种方法:
(1)同步代码块
(2)同步方法
(3)lock显示锁
a.同步代码块
作用:把出现线程安全的核心代码块进行上锁,每次只能一个线程进来执行完毕才能解锁,让其他线程进来执行。
格式: synchronized(锁对象){
//访问共享资源的代码块
}
//注意:同步会影响代码效率和性能,锁的代码块越精细越好。
锁对象:”唯一“对象
锁对象建议使用共享资源。
---在实例方法中,建议使用this作为锁对象,因为此时this正好是共享资源,代码必须高度面向对象
---在静态方法中,建议使用 类名.class 作为锁对象。
b.同步方法
作用:把出现线程安全的核心方法进行上锁。每次只能一个线程进来执行。
用法:直接给一个方法上加修饰符 synchronized
原理:同步方法和同步代码块底层原理一样。
同步方法其实底层也有锁对象。
如果方法是实例方法,同步方法默认用this作为锁对象
如果方法为静态方法,同步方法名默认用类名.class作为锁对象
c.lock显示锁(同步锁)
java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized同步方法更为广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此以外更强大。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
void lock() 获取锁。 void unlock() 释放锁。
模拟代码(此时为线程非安全):
//账户类
public class Account {
private String name;
private double money;
//定义取钱方法
public void drawmoney(double money) {
String name=Thread.currentThread().getName();
if(this.money>=money){
System.out.println(name+"取出了"+money);
this.money-=money;
System.out.println(name+"取完了还剩"+this.money);
}else
{
System.out.println(name+"来取钱余额不足");
}
}
public Account(String name, int money) {
this.name = name;
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"name='" + name + '\'' +
", money=" + money +
'}';
}
public Account() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
}
//线程任务类
public class DrawThread implements Runnable{
private Account acc;
public DrawThread(Account acc){
this.acc=acc;
}
@Override
public void run() {
acc.drawmoney(10000);
}
}
//主类
public class ThreadSafe {
public static void main(String[] args) {
Account account=new Account("工商银行",10000);
Runnable runnable=new DrawThread(account);
Thread xiaoming=new Thread(runnable,"小明线程");
Thread xiaohong=new Thread(runnable,"小红线程");//两个线程接同一个账户
xiaoming.start();
xiaohong.start();
}
}
此时会出现线程安全问题:
打印输出为:
小明线程取出了10000.0
小红线程取出了10000.0
小红线程取完了还剩-10000.0
小明线程取完了还剩0.0
a用同步代码块的方式上锁:
synchronized (this){
if(this.money>=money){
System.out.println(name+"取出了"+money);
this.money-=money;
System.out.println(name+"取完了还剩"+this.money);
}else
{
System.out.println(name+"来取钱余额不足");
}
}
或者在调用方法时上锁,也一样
@Override
public void run() {
synchronized (this){
acc.drawmoney(10000);
}
}
输出结果:
小明线程取出了10000.0
小明线程取完了还剩0.0
小红线程来取钱余额不足
b同步方法上锁:
public synchronized void drawmoney(double money) {
String name=Thread.currentThread().getName();
if(this.money>=money){
System.out.println(name+"取出了"+money);
this.money-=money;
System.out.println(name+"取完了还剩"+this.money);
}else
{
System.out.println(name+"来取钱余额不足");
}
}
输出结果:
小明线程取出了10000.0
小明线程取完了还剩0.0
小红线程来取钱余额不足
c.Lock锁 代码实现
private final Lock lock=new ReentrantLock(); //创建一把锁对象 注意Lock是接口
//定义取钱方法
public void drawmoney(double money) {
String name=Thread.currentThread().getName();
lock.lock(); //上锁
try {
if(this.money>=money){
System.out.println(name+"取出了"+money);
this.money-=money;
System.out.println(name+"取完了还剩"+this.money);
}else
{
System.out.println(name+"来取钱余额不足");
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
//用了try catch finally是为了避免一个线程出现异常而导致后面线程都无法运行的情况
}
}
打印输出:
小明线程取出了10000.0
小明线程取完了还剩0.0
小红线程来取钱余额不足
1.5线程通信
因为多个线程在一个进程中,所以互相通信比较容易。
线程通信经典模型:生产—消费者模型
生产者负责生产商品,消费者负责消费商品。
生产不能过剩,消费不能没有。
模拟案例:
小明和小红有一个共同账户:共享资源
他们有三个家人给他们存钱。
模型:小明和小红去取钱,如果有钱就取出,然后等待自己,唤醒三个家长来存钱
家长来存钱,有钱就不存,没钱就存,然后等待自己,唤醒自己的孩子来取钱
分析:
生产者线程:家长1,家长2,家长3
消费者线程:小明,小红
共享资源:账户
注意:线程通信一定是多个线程在操作同一资源时才需要通信。
线程通信必须先保证线程安全!否则毫无意义,代码也会报错。
线程通信的核心方法;
public void wait(); 让当前线程进入等待状态 此方法必须锁对象调用
public void notify(); 唤醒当前锁对象上等待状态的某个线程,此方法必须锁对象调用
public void notifyAll(); 唤醒当前锁对象上等待状态的全部线程,此方法必须锁对象调用
1.6线程状态
1.7线程池
什么是线程池?
线程池其实就是容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
线程池的作用?
1.降低资源消耗。
--减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
2.提高响应速度
--不需要频繁的创建线程,如果有线程可以直接用,避免系统僵死
3.提高线程的可管理性
--线程池可以约束系统最多有几个线程,不会因为线程过多而死机
线程池的核心思想:线程复用,同一个线程可以被多次使用。
线程池所涉及的类和接口:
Executor
线程池顶级接口。
常用方法 - void execute(Runnable)
作用是: 启动线程任务的。ExecutorService
Executor 接口的子接口。
常见方法 - Future submit(Callable), Future submit(Runnable)Future
未来结果,代表线程任务执行结束后的结果。Callable
可执行接口。
接口方法 : Object call();相当于 Runnable 接口中的 run 方法。区别为此方法有返回值。不能抛出已检查异常。
和 Runnable 接口的选择 - 需要返回值或需要抛出异常时,使用 Callable,其他情况可任意选择。Executors
工具类型。为 Executor 线程池提供工具方法。类似 Arrays,Collections 等工具类型的功用。FixedThreadPool
容量固定的线程池
queued tasks - 任务队列
completed tasks - 结束任务队列CachedThreadPool
缓存的线程池。容量不限(Integer.MAX_VALUE)。自动扩容。默认线程空闲 60 秒,自动销毁。ScheduledThreadPool
计划任务线程池。可以根据计划自动执行任务的线程池。SingleThreadExceutor
单一容量的线程池。ForkJoinPool
分支合并线程池(mapduce 类似的设计思想)。适合用于处理复杂任务。初始化线程容量与 CPU 核心数相关。
线程池中运行的内容必须是 ForkJoinTask 的子类型(RecursiveTask,RecursiveAction)。WorkStealingPool
JDK1.8 新增的线程池。工作窃取线程池。当线程池中有空闲连接时,自动到等待队列中窃取未完成任务,自动执行。
初始化线程容量与 CPU 核心数相关。此线程池中维护的是精灵线程。
ExecutorService.newWorkStealingPool();ThreadPoolExecutor
线程池底层实现。除 ForkJoinPool 外,其他常用线程池底层都是使用 ThreadPoolExecutor
实现的。
public ThreadPoolExecutor
(int corePoolSize, // 核心容量int maximumPoolSize, // 最大容量
long keepAliveTime, // 非核心线程最长空闲存活时间
TimeUnit unit, // 生命周期单位
BlockingQueue workQueue // 任务队列,阻塞队列。
);
线程池的创建:
一个有四种创建线程的方式,这里只用了
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
提交Runnable任务:
public class ThreadPool {
public static void main(String[] args) {
//a.创建一个线程池并指定线程条数
ExecutorService pools = Executors.newFixedThreadPool(3);
//b.提交任务
Runnable target = new Myrunnable();
pools.submit(target); //提交一个Runnable任务并执行
pools.submit(target);
pools.submit(target);
pools.submit(target);//此次复用之前的某条线程
pools.shutdown();//等线程执行完关闭线程池
// pools.shutdownNow();//立即关闭线程池
}
}
class Myrunnable implements Runnable{
@Override
public void run() {
for(int i=0;i<5;i++)
System.out.println(Thread.currentThread().getName()+"=====》"+i);
}
}
Callable任务:
public class ThreadPool2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//a.创建一个线程池并指定线程条数
ExecutorService exector= Executors.newFixedThreadPool(3);
//b.提交Callable的任务对象后返回一个未来任务对象
Future<String> f1=exector.submit(new Mycallable(100));
Future<String> f2=exector.submit(new Mycallable(200));
Future<String> f3=exector.submit(new Mycallable(300));
//c.获取线程池执行的结果
String str=f1.get(); //要抛出异常
String st2=f2.get();
String st3=f3.get();
System.out.println(str);
System.out.println(st2);
System.out.println(st3);
}
}
class Mycallable implements Callable<String>{
private int n;
public Mycallable(int n){
this.n=n;
}
//需求:使用线程池计算1-100,1-200,1-300的和并返回
@Override
public String call() throws Exception {
int sum=0;
for(int i=0;i<n;i++)
{
sum+=i;
}
return Thread.currentThread().getName()+"从1到"+n+"的执行结果为===>"+sum;
}
}
注意:线程池里面除了sumbit()还有execute()方法可以提交线程
execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。
execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。
其他后续补充
1.8死锁
什么是死锁?
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
死锁产生条件:
1.互斥:当资源被一个线程占用时,别的线程不能使用
2.请求和保持:资源请求者在请求其他资源时,仍保持对原占有资源的占用。
3.不可抢占: 资源请求者不能强制从资源占有者里剥夺资源,资源只能由资源占有者释放。
4.循环等待:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
以上四个条件必须同时满足才会死锁,反之只要有一个条件不满足就能破坏死锁。
1.9 volatile关键字
并发编程下,多线程修改变量,会出现线程间变量的不可见性。
不可见性的内存语义:
JMM(Java Memory Model ) Java内存模型。是java虚拟机规范所定义的一种内存模型,java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
//其实就是抽离于栈、堆,专门为并发编程搞出来的一种模型 :)
Java内存模型(Java Memory Model )描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和内存中读取变量这样的底层细节。
JMM有以下规定:
-
所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
-
每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本
-
线程对变量的所有操作(读、取)都必须在工作内存中完成,而不能直接读写主内存中的变量
-
不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成
本地内存和主存的关系:
变量不可见性的原因:
每个线程都有自己的工作内存,线程都是从主内存拷贝共享变量的副本值。
每个线程是在自己的工作内存中操作共享变量的。
变量不可见性的解决方案:
两种常见方式:
1.加锁
加锁会清空工作内存,读取主内存的最新值到工作内存中来。
2.对共享变量加volatile关键字
volatile是当一个子线程修改它修饰的变量时,子线程把修改后的变量传回给主内存,其他之前调用了volatile修饰的变量的线程会得到值已经改变,让旧值失效,去主内存读取最新值。
--即一旦一个线程修改了volatile修饰的变量,另一个线程可以立即取到最新值。
volatile和synchronized的区别
-
volatile只能修饰实例变量和类变量。而synchronized可以修饰方法和代码块。
-
volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。
那什么是原子性呢?
原子性即一个操作或多个操作,要么全部执行并且执行的过程不会被中断,要么就都不执行,原子性就像数据库里面的事务一样,他们是一个团队,同生共死。(博主已经放弃贴代码了:(
volatile只能保证数据的可见性,不能保证原子性,那怎么保证原子性呢?
保证原子性的方案
1.加锁(注意加锁机制性能差)所以要存在其他方法
2.原子类
什么是原子类?
java 1.5引进原子类,具体在java.util.concurrent.atomic包下,atomic包里面一共提供了13个类,分为4种类型,分别是:原子更新基本类型,原子更新数组,原子更新引用,原子更新属性。原子类也是java实现同步的一套解决方案。
原子类常用API
// 以原子方式将给定值与当前值相加,可用于线程中的计数使用,(返回更新的值)。 int addAndGet(int delta) // 以原子方式将给定值与当前值相加,可用于线程中的计数使用,(返回以前的值) int getAndAdd(int delta) // 以原子方式将当前值加 1(返回更新的值) int incrementAndGet() // 以原子方式将当前值加 1(返回以前的值) int getAndIncrement() // 以原子方式设置为给定值(返回旧值) int getAndSet(int newValue) // 以原子方式将当前值减 1(返回更新的值) int decrementAndGet() : // 以原子方式将当前值减 1(返回以前的值) int getAndDecrement() // 获取当前值 get()
原子类一览:(之前的区别和用处之后再学习补充)
原子类CAS机制实现线程安全
Atomic操作类的底层正是用到了“CAS机制”(Compare and Swap)即比较再交换,CAS是现代广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS可以将read—modify—check—write 转换为原子操作,这个原子操作直接由处理器保证。
CAS和Synchronized
从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
当然还有其他各种锁,需要进一步深入学习。
CAS存在的问题
1) CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2) 不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3) ABA问题
这是CAS机制最大的问题所在。(后面有介绍)
23. 并发包
并发包的来历:
在实际开发中如果不需要考虑线程安全问题,就不要做线程安全,因为线程安全是消耗性能的,
但是实际开发中线程安全问题是大部分时候要考虑的,所以Java为很多业务场景提供了性能优异,
且线程安全的并发包,让程序员可以选择使用!
2.1ConcurrentHashMap
Map集合中的经典集合:HaspMap是线程不安全的,性能好。
Hashtable线程安全,加入了计时,但是性能差。(源码里几乎每个方法都加了synchronized)
ConcurrentHashMap线程安全,综合性能好。
模拟代码:
public class ConcurrentHashMapDemo {
//演示HashMap在高并发下的线程不安全性
public static Map<String, String> maps = new HashMap<>(); //线程不安全
//public static Map<String, String> maps = new ConcurrentHashMap<>(); 线程安全性能好
public static void main(String[] args) {
Runnable target = new Myrunnable();
Thread t1=new Thread(target,"线程1");
Thread t2=new Thread(target,"线程2");
t1.start();
t2.start();
try {
t1.join();
t2.join(); //让t1,t2跑完,不让主线程抢占cpu,但是t1,t2会抢占cpu,目的是保证两个线程能肯定跑完
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("元素个数"+maps.size());
}
}
class Myrunnable implements Runnable{
@Override
public void run() {
for(int i=0;i<500;i++)
ConcurrentHashMapDemo.maps.put(Thread.currentThread().getName()+i,Thread.currentThread().getName()+i); //保证key不重复
}
}
输出:元素个数991 (不为1000!会丢失数据
而使用ConcurrentHashMap打印为1000,增删改查都能安全执行,不会丢失数据
2.2ContDownLatch
ContDownLatch运行一个或多个线程等待其他线程完成操作,再执行自己。
例如:线程A要执行打印功能打印A,C,但是B线程要打印B,想要线程B打印完B后再打印A那就要去等待线程B打印
CountDownLatch 定义了一个计数器,和一个阻塞队列, 当计数器的值递减为0之前,阻塞队列里面的线程处于挂起状态,当计数器递减到0时会唤醒阻塞队列所有线程,这里的计数器是一个标志,可以表示一个任务一个线程,也可以表示一个倒计时器,CountDownLatch可以解决那些一个或者多个线程在执行之前必须依赖于某些必要的前提业务先执行的场景。
CountDownLatch常用API
CountDownLatch(int count); //构造方法,创建一个值为count 的计数器。
await();//阻塞当前线程,将当前线程加入阻塞队列。
await(long timeout, TimeUnit unit);//在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行,
countDown();//对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
模拟代码:
public class CountDownLatchDemo {
public static void main(String[] args) {
//定义一个CountDownLatch对象
CountDownLatch c=new CountDownLatch(1);
new ThreadA(c).start();
new ThreadB(c).start();
}
}
class ThreadA extends Thread{
private CountDownLatch c;
public ThreadA(CountDownLatch c) {
this.c = c;
}
@Override
public void run() {
System.out.println("A");
try {
c.await(); //;//阻塞当前线程,将当前线程加入阻塞队列。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C");
}
}
class ThreadB extends Thread{
private CountDownLatch c;
public ThreadB(CountDownLatch c) {
this.c = c;
}
@Override
public void run() {
c.countDown();//计数器-1
System.out.println("B");
}
}
打印输出:A B C
2.3CyclicBarrier
CyclicBarrier是什么?
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
public class CyclicbarrierDemo {
public static void main(String[] args) {
//创建循环屏障,让五个线程都到达时才执行任务
//CyclicBarrier(int parties, Runnable barrierAction)
// 创建一个新的 CyclicBarrier,
// 它将在给定数量的参与者(线程)处于等待状态时启动,
// 并在启动 barrier 时执行给定的屏障操作,
// 该操作由最后一个进入 barrier 的线程执行。
CyclicBarrier cyclicBarrier=new CyclicBarrier(5,new Metting());
for(int i=1;i<=5;i++){
new EmployeeThread("员工"+i,cyclicBarrier).start();
}
}
}
class Metting implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始组织会议!");
}
}
class EmployeeThread extends Thread{
private CyclicBarrier cyclicBarrier;
public EmployeeThread(String name,CyclicBarrier cyclicBarrier){
super(name);
this.cyclicBarrier=cyclicBarrier;
}
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName()+"正在进入会议");
cyclicBarrier.await();//在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待
} catch (Exception e) {
e.printStackTrace();
}
}
}
员工1正在进入会议
员工4正在进入会议
员工3正在进入会议
员工5正在进入会议
员工2正在进入会议
员工2开始组织会议!
2.4Semaphore
semaphore(发信号)的主要作用是控制线程并发占锁的数量。
synchronized可以起到“锁”的作用,但某个时间段内,只能有一个线程允许执行。
Semaphore可以设计同时允许好几个线程执行。
Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。
构造方法摘要 | |
---|---|
Semaphore(int permits) 创建具有给定的许可数和非公平的公平设置的 Semaphore 。 | |
Semaphore(int permits, boolean fair) 创建具有给定的许可数和给定的公平设置的 Semaphore 。 permits表示可允许的线程数,fair为true表示下次执行的线程等待最久 |
方法摘要 | |
---|---|
void | acquire() 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。 |
void | release() 释放一个许可,将其返回给信号量。 |
public class SemaphoreDemo {
public static void main(String[] args) {
Service service=new Service();
for(int i=1;i<=5;i++){
Thread a=new Mythread(service);
a.start();
}
}
}
class Mythread extends Thread{
private Service service;
public Mythread(Service service) {
this.service=service;
}
@Override
public void run() {
try {
service.login();
} catch (InterruptedException e) {
e.printStackTrace();
}
;
}
}
class Service{
//1表示最多允许一个线程执行acquire()和release()之间的内容
private Semaphore semaphore=new Semaphore(1);
public void login() throws InterruptedException {
semaphore.acquire(); //上锁
System.out.println(Thread.currentThread().getName()+"进入时间为"+System.currentTimeMillis());
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"登录成功");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"离开时间为"+System.currentTimeMillis());
semaphore.release();//释放锁
}
}
Thread-0进入时间为1649480391716
Thread-0登录成功
Thread-0离开时间为1649480392726
Thread-2进入时间为1649480392726
Thread-2登录成功
Thread-2离开时间为1649480393736
Thread-1进入时间为1649480393736
Thread-1登录成功
Thread-1离开时间为1649480394738
Thread-4进入时间为1649480394738
Thread-4登录成功
Thread-4离开时间为1649480395744
Thread-3进入时间为1649480395744
Thread-3登录成功
Thread-3离开时间为1649480396749Process finished with exit code 0
其实这个类感觉主要还是用来控制流量的,用来控制一次可以执行几个线程并发,不是为了安全目的。
2.5Exchanger
Exchanger(交换者)是一个线程间协作的工具类。Exchanger用于进行线程间的数据交换。
两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,他会一直等待第二个线程也执行exchange方法。
当两个线程都达到同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
常用应用场景:数据校对
方法摘要 | |
---|---|
V | exchange(V x) 等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。 |
V | exchange(V x, long timeout, TimeUnit unit) 等待另一个线程到达此交换点(除非当前线程被中断,或者超出了指定的等待时间),然后将给定的对象传送给该线程,同时接收该线程的对象。 |
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> exchanger=new Exchanger();
new Boy(exchanger).start();
new Girl(exchanger).start();
}
}
class Boy extends Thread{
private Exchanger<String> exchanger;
public Boy(Exchanger exchanger) {
this.exchanger=exchanger;
}
@Override
public void run() {
System.out.println("男孩开始做自己的定情信物");
try {
String rs=exchanger.exchange("男孩的定情信物");
System.out.println("男孩收到礼物:"+rs);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Girl extends Thread{
private Exchanger<String> exchanger;
public Girl(Exchanger exchanger) {
this.exchanger=exchanger;
}
@Override
public void run() {
System.out.println("女孩开始做自己的定情信物");
try {
String rs=exchanger.exchange("女孩的定情信物");
System.out.println("女孩收到礼物:"+rs);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
男孩开始做自己的定情信物
女孩开始做自己的定情信物
女孩收到礼物:男孩的定情信物
男孩收到礼物:女孩的定情信物
24.Lambda表达式
什么是Lambda表达式?
Lambda表达式是JDK1.8之后的新语法,核心目的是简化匿名内部类的代码写法。
Lambda表达式格式:
(匿名内部类被重写方法的形参列表)-> { 被重写方法的方法体代码。 }
注意:Lambda不能简化所以的匿名内部类
Lambda表达式只能简化接口中只有一个抽象方法的匿名内部类形式
Lambda表达式只能简化函数式接口的匿名内部类写法:
a.必须是接口(因此它不能简化Thread,但是可以简化Runnable
b.接口中只能有一个抽象方法
@FunctionalInterface函数式接口注解:
一旦某个接口加上了这个注解,这个接口只能有且一个抽象方法,这个接口就可以被Lambda表达式简化。(Runnable接口就有这个注解哦)
public class LambdaDemo {
public static void main(String[] args) {
new Thread/*(new Runnable */
(() ->
/* @Override
public void run()*/ {
System.out.println(Thread.currentThread().getName()+"执行成功");
}).start();
}
}
Collections.sort(lists, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge();
}
});
//简化Comparator接口
Collections.sort(lists,(Student o1, Student o2)) ->{
return o1.getAge() - o2.getAge();
});
当然,Lambda表达式还可以进一步省略,(个人不喜欢= =)不再赘述了,感兴趣的童鞋度娘哦~
25.方法引用
什么是方法引用?
方法引用时为了进一步简化Lambda表达式的写法
方法引用的格式: 类型或者对象::引用方法
关键语法是: “::”
public class Test01 {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("小王");
list.add("小明");
list.add("小红");
// list.forEach(s-> System.out.println(s));
list.forEach(System.out::println);
}
}
方法引用有四种形式:
1.静态方法的引用
2.实例方法的引用
3.特定类型方法的引用
4.构造器引用
因为简化代码可读性比较差,个人不是很喜欢,这里不再贴写代码注释了。。
26.Stream流
什么是Stream流?
在Java 8中,得益于Lambda所带来的函数式编程,
引入了一个全新的Stream流概念,用于解决已有的集合/数组类库有的弊端。
Stream流的作用?
可以解决已有集合类库或者数组API的弊端
Stream认为集合和数组操作的API很不好用,所以采用了Stream流简化集合和数组的操作。
//需求;从集合中找出姓张的人,并且长度为3
public class Stream01 {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("张王笑");
list.add("李明");
list.add("孙红");
list.add("张红");
//stream可以链式编程
list.stream().filter(s -> s.startsWith("张")).filter(s -> s.length()==3).forEach(s -> System.out.println(s));
//其实我感觉普通写法也不烦啊= =
list.forEach(s->{if(s.startsWith("张")&&s.length()==3) System.out.println(s);});
}
}
Stream流的思想核心?
Stream流其实就是一根传送带,把集合或者数组送到传送带上,然后在上面操作集合或者数组的元素,一条线完成。从而简化流程。
集合或数组获取Stream流的方式:
public class Stream02 {
public static void main(String[] args) {
/**-----------------Collection集合获取流--------------**/
Collection<String> c=new ArrayList<>();
Stream<String> ss=c.stream();
/**-----------------Map集合获取流--------------**/
Map<String,Integer> map=new HashMap<>();
//获取键的Stream流
Stream<String> keyss=map.keySet().stream();
//获取值的Stream流
Stream<Integer> valuess=map.values().stream();
//获取键值对的Stream流
Stream<Map.Entry<String,Integer>> keyAndValues=map.entrySet().stream();
/**-----------------数组获取流--------------**/
String arrs[]=new String[]{"XIAOMING","XIAOWANG"};
Stream<String> s1= Arrays.stream(arrs);
Stream<String> s2=Stream.of(arrs); //两种都可以拿到
}
}
Stream流的常用API
public class Stream03 {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("张王笑");
list.add("李明");
list.add("孙红");
list.add("张红");
//stream可以链式编程
list.stream().filter(s -> s.startsWith("张")).filter(s -> s.length()==3).forEach(s -> System.out.println(s));
//count统计个数 注意count是long型
long count=list.stream().filter(s -> s.startsWith("张")).filter(s -> s.length()==3).count();
//limit取前几个元素
list.stream().filter(s -> s.startsWith("张")).filter(s -> s.length()==3).limit(2);
//skip跳过前几个元素
list.stream().filter(s -> s.startsWith("张")).filter(s -> s.length()==3).skip(2);
//map加工方法(把原来的元素加工后重新放上去)
//名字前加192班
list.stream().map(s->"192班的:"+s).forEach(System.out::println);
//concat合并
// 新建一个数组流
Stream<Integer> s1=Stream.of(10,20,30,40);
Stream<String> s2=list.stream();
//将s1和s2流合并
Stream<Object> s3=Stream.concat(s1,s2);//注意,因为s1和s2类型不一样,所以这里是Object类型
}
}
终结方法和非终结方法
终结方法:一旦Stream调用了终结方法,流的操作就全部终结了,不能继续使用,只能创建新的Stream方法,终结方法有:foreach,count
非终结方法:每次调用完成以后返回一个新的流对象,可以继续使用,支持链式编程!
简单来说:调用了终究方法就是拿下来传输带了,不能在继续用了,非终结方法每次调用完会放回传输带,你可以继续调用其他方法。
收集Stream流:把Stream流的数据转回成集合。
Stream流只是操作手段,集合才是得到的最终目标。
//Stream流转换成List集合
Stream<String> stream01=list.stream().filter(s->s.length()==3);//流只能用一次,这里要重新取个流
List<String> list01=stream01.collect(Collectors.toList());
//Stream流转换成数组
Stream<String> stream02=list.stream().filter(s->s.length()==3);
Object[] arr=stream02.toArray();
// 可以借用构造器引用来申明转换成的数据类型
String[] arrr=stream02.toArray(String[]::new);
//Stream流转换成Set集合
Stream<String> stream03=list.stream().filter(s->s.length()==3);
Set<String> set=stream03.collect(Collectors.toSet());
27.File类
什么是Fiile类:
File类是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹0等
包:java.io.File
构造方法摘要 | |
---|---|
File(File parent, String child) 根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。 | |
File(String pathname) 通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例。 | |
File(String parent, String child) 根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例 |
File常用的API
public static void main(String[] args) throws IOException {
//文件路径分隔符可以使用“/”,或者“\\”或者分隔符api File.separator
File file=new File("src/File/a.txt");//注意,相对路径只能找当前工程下的文件,一般用相对路径比较好可以跨平台
//a.获取文件的绝对路径getAbsolutePath
System.out.println(file.getAbsolutePath());
//b.获取文件定义时的路径getPath
System.out.println(file.getPath());
//获取文件名称,带后缀getName
System.out.println(file.getName());
//获取文件长度length
System.out.println(file.length());
/*-----------------判断API--------*/
//a.判断文件路径是否存在
System.out.println(file.exists());
//b.判断是否为文件
System.out.println(file.isFile());
//c.判断是否为文件夹
System.out.println(file.isDirectory());
/*-----------------创建和删除API--------*/
File file2=new File("src/File/b.txt");
//a.创建新文件,创建成功返回true
System.out.println(file2.createNewFile());//要抛出IO异常
//b.删除文件或者空文件夹
System.out.println(file2.delete());
//c.创建一级目录
File file3=new File("D:/a目录");
System.out.println(file3.mkdir());
//d.创建多级目录
File file4=new File("D:/a目录/n目录/c目录");
System.out.println(file4.mkdirs());
long time=file4.lastModified();//最后修改时间
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(time));
}
File目录的遍历
//获取当前目录下所有的“一级文件名称”到一个字符串数组中去返回
File dir=new File("D:/AAA");
String[] name=dir.list();
for (String s : name) {
System.out.println(s);
}
//获取当前目录下所有的“一级文件名称”到一个文件对象数组中去返回(常用)
File[] files=dir.listFiles();
for (File filess : files) {
//filess.delete();
System.out.println(filess.getAbsolutePath());
}
28.递归
什么是递归?
递归就是在方法里面又调用自己方法。
递归分为直接递归和间接递归:
直接递归:自己的方法调用自己
间接递归:自己的方法调用其他方法,其他方法调用自己
注意:递归使用不当会进入死循环~! 递归的方法必须走向终结点!
递归主要还是要得到公式和结果值
案例:求1~n的和
f(n)=1+2+3+4......+n-1+n;
推出:f(n)=f(n-1)+1;
//公式:f(n)=f(n-1)+n
//终结点;f(n)=1
//递归方向走向终结点
public class ee {
public static void main(String[] args) {
System.out.println(f(10));
}
public static int f(int n) {
if (n == 1) {
return 1;
}else
{
return f(n-1)+n;
}
}
}
非公式下的递归案例:文件查询
import java.io.File;
2
3 /**
4 * @Author: heq
5 * @Date: 2020/6/23 20:51
6 */
7 public class Test {
8 public static void main(String[] args) {
9 File file = new File("C:");//查找的盘符
10 heq(file);
11 }
12
13 public static void heq(File file) {
14 File[] files = file.listFiles();
15 if (files != null) {
16 for (File file1 : files) {
17 if (file1.isFile() && file1.getName().endsWith(".wmv")) {//wmv查找的格式
18 System.out.println(file1.getName());
19 }
20 if (file1.isDirectory()) {
21 heq(file1);
22 }
23 }
24 }
25 }
26 }
总结:递归可以用在有数学公式支持的情况下,当然还有一些别的场景也能用到递归,自己视情况而定,递归是很消耗内存的,注意使用递归的时候要防范死锁。
29.IO流(4文件4缓冲2编码)
知识基础(字符集/编码集)
字符集:各个国家为自己国家的字符取的一套编号规则。
计算机的底层是不能直接存储字符的。
计算机底层只能存储二进制。
二进制可以转换为十进制。十进制就是整数编号。
结论:计算机底层可以存储编号。
1Byte(字节)=8bit
英文和数字在底层存储的时候都是采用1个字节存储的。
GBK编码一个中文一般占两个字节。
UTF-8编码(又称万国码)一个中文一般占三个字节。
引入:File类只能操作文件对象本身,不能读写文件对象的内容。
读写数据内容,需要IO流!!
IO流是一个水流模型,IO理解成水管,数据理解成水流。
IO流的分类
1.按流向划分,分为输入流、输出流
输出流:以内存为基准,把内存中的数据写出到磁盘文件或者网络介质中去的流称为输出流。
输出流的作用:写数据到文件,或者写数据发送给别人
输入流:以内存为基准,把磁盘文件中的数据或者网络中的而数据读入到内存中的流称为输入流。
输入流的作用:读取数据到内存。
2.按操作的数据单元类型划分,分为字节流、字符流
字节流操作的数据单元是8位的字节(byte),字符流操作的是16位的字符。
字节流:二进制,可以处理一切文件,包括:纯文本、doc、音频、视频等。
字符流:文本文件,只能处理纯文本。
请注意,系统输入输出(System.in与System.out)都为字节流。
所以流大体分为四大类:
抽象类👇 实现类👇 实现类👇(性能高) 实现类👇
字节输入流; InputStream FileInputStream BufferedInputStream (处理编码
字节输出流; OutputStream FileOutputStream BufferedOutputStream
字符输入流; Reader FileReader BufferedReader InputStreamReader
字符输出流; Writer FileWriter BufferedWriter OutputStreamReader
a.FileInputStream文件字节输入流
按照字节读取文件数据到内存
构造方法摘要 | |
---|---|
FileInputStream(File file) 通过打开一个到实际文件的连接来创建一个 FileInputStream ,该文件通过文件系统中的 File 对象 file 指定。 | |
FileInputStream(FileDescriptor fdObj) 通过使用文件描述符 fdObj 创建一个 FileInputStream ,该文件描述符表示到文件系统中某个实际文件的现有连接。 | |
FileInputStream(String name) 通过打开一个到实际文件的连接来创建一个 FileInputStream ,该文件通过文件系统中的路径名 name 指定。 |
拓展:其实第三个构造方法是在第一个构造方法上的延伸,但是当你需要用到File对象的方法时,最好还是使用第一个构造方法来获取。
public class FileInputStreamDemo {
public static void main(String[] args) throws IOException {
//1.创建文件对象取到a.txt
File file=new File("src/File/a.txt");
//2.创建一个字节输入流管道与文件对象接通
InputStream is=new FileInputStream(file);
//3.遍历读取
int ch=0;
while((ch=is.read())!=-1){
System.out.print((char)ch);
}
//4.定义一个桶
byte[] buffer=new byte[1024];
int len;//存储每次读取的字节数
while((len=is.read(buffer))!=-1){
String rs=new String(buffer,0,len); //读取多少就输出多少数据
System.out.println(rs);
}
}
}
总结:FileInputStream可以一个一个字节读取英文和数字,但是读取中文会出现乱码。
字节输入流并不适合读取文本文件的内容,读写文件内容应该使用字符流,字节流可以做文件复制,通信。
b.FileOutputStream文件字节输入流
public class FileOutputStreamDemo {
public static void main(String[] args) throws Exception {
OutputStream os=new FileOutputStream(new File("src/IO流/a.txt"));
//注意,管道默认是覆盖管道,就是你每次使用时会把数据清空。
OutputStream os2=new FileOutputStream(new File("src/IO流/a.txt"),true);
//而用此方法,append为true则将管道设置为了追加管道!!每次启动管道不会清空数据!
os.write(97);
os.write('s');
//os.write('我');//会乱码,此方法只能写出一个字节。
os.flush();//立即刷新数据到管道中去(生效数据),刷新后管道可以继续使用
//用桶
os.write("\r\n".getBytes()); //换行
byte[] bytes=new byte[]{97,98,99,100}; //注意这是字节码
os.write(bytes);
byte[] bytes1="你好世界".getBytes();//获取所有的字节
System.out.println(bytes1.length);
os.write(bytes1);
os.write("\r\n".getBytes()); //换行
os.write(bytes1,0,3); //输出三个字节
os.close();//关闭后不能再使用,close的时候会刷新的
}
}
as abcd你好世界 你
总结: 字节输出流可以写字节数据到文件中去。
可以写一个字节,写一个字节数组,或写字节数组的自定义长出去。
管道用完需要关闭,数据要生效需要刷新。关闭时候会刷新,刷新后流可以继续使用,关闭后流无法使用。
字节输出流管道默认是覆盖数据管道,启动管道写数据前会清空原先数据。
字节流实现文件的复制
因为字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制。
复制是把源文件的全部字节一字不漏的转移到目标文件,只要文件前后格式一样,绝对不会出现问题!
分析步骤:
(1)创建一个字节输入流管道与源文件连通
(2)创建一个字节输出流管道与目标文件连通
(3)创建一个字节数组作为桶
(4)从字节输入流管道读取数据到字节输出流
(5)关闭流
public class CopyDemo {
public static void main(String[] args) {
InputStream is=null;
OutputStream os=null;
try {
is=new FileInputStream("src/IO流/a.txt");
os=new FileOutputStream("src/IO流/b.txt");
byte[] buf=new byte[1024]; //1kb
int len;
while((len=is.read(buf))!=-1){
os.write(buf,0,len);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if(is!=null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(os!=null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
//这里发现释放资源用了很长的代码,下面会讲优化
}
}
}
}
JDK1.7以后 释放资源的方式
try-with-resource
try( //这里只能放资源对象,用完会自动调用close()关闭 ){ } catch(Exception e){ e.printStackTrace(); }
资源类是实现了Closeable接口的,实现了这个接口的类就是资源。
有close()方法,try-with-resource在执行完毕后会自动调用它的close()关闭资源。
之前的代码优化:
public class CopyDemo {
public static void main(String[] args) {
try( InputStream is=new FileInputStream("src/IO流/a.txt");
OutputStream os=new FileOutputStream("src/IO流/b.txt");
) {
byte[] buf=new byte[1024]; //1kb
int len;
while((len=is.read(buf))!=-1){
os.write(buf,0,len);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
c.FileReader文件字符输入流
构造方法摘要 | |
---|---|
FileReader(File file) 在给定从中读取数据的 File 的情况下创建一个新 FileReader。 | |
FileReader(FileDescriptor fd) 在给定从中读取数据的 FileDescriptor 的情况下创建一个新 FileReader。 | |
FileReader(String fileName) 在给定从中读取数据的文件名的情况下创建一个新 FileReader。 |
public class FilReaderDemo {
public static void main(String[] args) throws Exception {
Reader fr=new FileReader("src/IO流/a.txt");
int code=fr.read();//读取一个字符的编号返回
System.out.print((char)code);
int ch;
while((ch=fr.read())!=-1){
System.out.print((char)ch); //while循环一个一个读取,定义一个变量存储字符的编号
}
//定义一个字符数组(桶)
char[] buf=new char[1024];
//定义一个整数记录每次桶读取的字符数据量
int len;
while((len=fr.read(buf))!=-1){
String rs=new String(buf,0,len);
System.out.print(rs);
}
}
}
d.FileWriter文件字符输出流
public class FileWriterDemo {
public static void main(String[] args) throws Exception {
Writer fw=new FileWriter("src/IO流/b.txt"); //覆盖数据管道
Writer fw2=new FileWriter("src/IO流/b.txt",true); //追加数据管道
//写一个字符
fw.write(97);
fw.write('w');
fw.write('我');
fw.write("\r\n");//换行,不用再换成字节了
fw.write("你好世界");//还可以写字符串
fw.write("hello世界".toCharArray());//写出字符数组 一般用于桶
fw.write("你好世界",0,2);//写一部分字符串 你好
fw.close();
}
}
e.BufferedInputStream 字节缓冲输入流
API文档的解释:在创建 BufferedInputStream时,会创建一个内部缓冲区数组(默认为8kb)。在读取流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。
也就是说,Buffered类初始化时会创建一个较大的byte数组,一次性从底层输入流中读取多个字节来填充byte数组,当程序读取一个或多个字节时,可直接从byte数组中获取,当内存中的byte读取完后,会再次用底层输入流填充缓冲区数组。这种从直接内存中读取数据的方式要比每次都访问磁盘的效率高很多。
作用:可以把低级的字节输入流包装成一个高级ide缓冲字节输入流管道,从而提高字节输入流读数据的性能。
构造方法摘要 | |
---|---|
BufferedInputStream(InputStream in) 创建一个 BufferedInputStream 并保存其参数,即输入流 in ,以便将来使用。 | |
BufferedInputStream(InputStream in, int size) 创建具有指定缓冲区大小的 BufferedInputStream 并保存其参数,即输入流 in ,以便将来使用。 |
public class BufferedInputStreamDemo {
public static void main(String[] args) throws Exception{
//1.定义一个低级的输入流与源文件接通
InputStream is=new FileInputStream("src/IO流/a.txt");
//2.把低级的字节输入流包装成一个高级的缓冲字节输入流
BufferedInputStream bis=new BufferedInputStream(is);
//3.定义一个桶遍历数据输出
byte[] bytes=new byte[1024];
int len;
while((len=bis.read(bytes))!=-1){
System.out.println(new String(bytes,0,len));
}
}
}
f.BufferedOutputStream 字节缓冲输出流
public class BufferedOutputStreamDemo {
public static void main(String[] args) throws Exception {
OutputStream os=new FileOutputStream("src/IO流/b.txt");
BufferedOutputStream bos=new BufferedOutputStream(os);
bos.write(99);
bos.write('a');
bos.write("我爱中国".getBytes());
bos.close();
}
}
用缓冲字节流来复制文件
public class CopyDemo02 {
public static void main(String[] args) {
try( InputStream is=new FileInputStream("src/IO流/a.txt");
OutputStream os=new FileOutputStream("src/IO流/b.txt");
BufferedInputStream bis=new BufferedInputStream(is);
BufferedOutputStream bos=new BufferedOutputStream(os);
) {
byte[] buf=new byte[1024]; //1kb
int len;
while((len=bis.read(buf))!=-1){
bos.write(buf,0,len);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
g.BufferedReader 字符缓冲输入流(常用)
构造方法摘要 | |
---|---|
BufferedReader(Reader in) 创建一个使用默认大小输入缓冲区的缓冲字符输入流。 | |
BufferedReader(Reader in, int sz) 创建一个使用指定大小输入缓冲区的缓冲字符输入流。 |
新方法:
String | readLine() 读取一个文本行。 |
public class BufferedReaderDemo {
public static void main(String[] args) throws Exception {
Reader fr=new FileReader("src/IO流/a.txt");
//定义一个字符串变量存储每行数据
//注意这是BufferedReader独有的API 这是一个经典代码!!!
// String readLine()读取一个文本行。
BufferedReader bfr=new BufferedReader(fr);
String line;
while((line=bfr.readLine())!=null){
System.out.println(line);
}
}
}
这是一个经典代码用来读取文本,有了缓存区性能更好,能够读取文本行更符合业务需求,而且能够完整读出文本的格式,还能读出空行!
h.BufferedWriter 字符缓冲输出流(常用)
字符缓存输出流除了提高字符输出流写数据的性能,还多了一个换行的特有功能。
public void newLine() : 新建一行
public class BufferedWriterDemo {
public static void main(String[] args) throws Exception {
Writer fw=new FileWriter("src/IO流/b.txt"); //覆盖数据管道
Writer fw2=new FileWriter("src/IO流/b.txt",true); //追加数据管道,注意Buffer只是提高性能,追加还是再原管道
BufferedWriter bw=new BufferedWriter(fw);
//写一个字符
bw.write(97);
bw.write('w');
bw.write('我');
bw.write("\r\n");//换行,不用再换成字节了
bw.write("你好世界");//还可以写字符串
bw.newLine(); //换行
bw.write("hello世界".toCharArray());//写出字符数组 一般用于桶
bw.newLine(); //换行
bw.write("你好世界",0,2);//写一部分字符串 你好
bw.close();
}
}
i.InputStreamReader 字符输入转换流
作用: 可以解决字符流读取不同编码乱码问题!
把原始的字节流按照当前默认的代码编码转换成字符输入流
也可以把原始的字节流按照指代编码转换成字符输入流
构造方法摘要 | |
---|---|
InputStreamReader(InputStream in) 创建一个使用默认字符集的 InputStreamReader。 几乎不用!! | |
InputStreamReader(InputStream in, Charset cs) 创建使用给定字符集的 InputStreamReader。 | |
InputStreamReader(InputStream in, CharsetDecoder dec) 创建使用给定字符集解码器的 InputStreamReader。 | |
InputStreamReader(InputStream in, String charsetName) 创建使用指定字符集的 InputStreamReader。 |
public class InputStreamReaderDemo {
public static void main(String[] args) throws Exception{
//提起文件的原始字节流
InputStream is=new FileInputStream("src/IO流/a.txt");
//把原始字节输入流通过转换流,转换成字符输入转换流InputStreamReader
Reader isr=new InputStreamReader(is,"UTF-8");
//包装成缓冲流
BufferedReader bfr=new BufferedReader(isr);
String line;
while((line=bfr.readLine())!=null){
System.out.println(line);
}
}
}
j.OutputStreamWriter字节输入转换流
作用:可以指定写出的字符编码
public class OutputStreamWriterDemo {
public static void main(String[] args) throws Exception{
//写一个字节输出流通向文件
OutputStream os=new FileOutputStream("src/IO流/b.txt");
//把字节输出流转换为字符输出流
Writer osw=new OutputStreamWriter(os,"UTF-8");
//包装成缓冲流
BufferedWriter bfr=new BufferedWriter(osw);
bfr.write("你好世界");
}
}
30.对象序列化 (临靠IO流)
属于大型框架底层的东西,我们一般不会怎么自己用。
对象序列化:把Java对象数据直接存储到文件中去。 对象=>文件中
对象反序列化:把对象的文件数据回到对象。 文件中=>对象
对象序列化流:ObjectOutputStream
父类:
java.io.OutputStream
构造方法摘要 | |
---|---|
| ObjectOutputStream(OutputStream out) 创建写入指定 OutputStream 的 ObjectOutputStream。 |
//注意:如果对象想实现序列化,对象必须实现序列化接口!!! implements Serializable
public class Student implements Serializable {
private String studname;
private int stuid;
public Student(String studname, int stuid) {
this.studname = studname;
this.stuid = stuid;
}
@Override
public String toString() {
return "Student{" +
"studname='" + studname + '\'' +
", stuid=" + stuid +
'}';
}
public String getStudname() {
return studname;
}
public void setStudname(String studname) {
this.studname = studname;
}
public int getStuid() {
return stuid;
}
public void setStuid(int stuid) {
this.stuid = stuid;
}
}
public class ObjectOutputStreamDemo {
public static void main(String[] args) throws Exception {
//创建一个学生对象
Student student=new Student("小明",1);
//创建低级的字节输出流通向目标文件
OutputStream os=new FileOutputStream("src/序列化/b.txt");
//把低级的字节输出流包装成高级流
ObjectOutputStream oos=new ObjectOutputStream(os);
// void writeObject(Object obj)将指定的对象写入 ObjectOutputStream。
oos.writeObject(student);
//关闭流
oos.close();
}
}
注意:要实现序列化的对象必须实现序列化接口 !!!!
对象反序列化ObjectInputStream
父类:
java.io.InputStream
public class ObjectInputStreamDemo {
public static void main(String[] args) throws Exception{
//定义一个低级流,拿到数据
InputStream is=new FileInputStream("src/序列化/b.txt");
//讲低级流包装成高级流
ObjectInputStream ojs=new ObjectInputStream(is);
//反序列化,当然这里可以最好先判断一些类型,再向下转型
Student student=(Student) ojs.readObject();
System.out.println(student);
}
}
拓展:如果对象有隐秘信息字段,不想参与序列化怎么办?
可以使用transient关键字修饰,被该词修饰的成员变量将不参与序列化!
如:private transient int stuid; 再反序列化时,该字段为打印为空!
序列化版本号
private static final long serialVersionUID=2L;
序列化使用的版本号和反序列化使用的版本号必须一致才可以正常序列化!如果不定义版本号,系统会默认给一个版本号
public class Student implements Serializable {
private static final long serialVersionUID=2L;
private String studname;
private transient int stuid;
public Student(String studname, int stuid) {
this.studname = studname;
this.stuid = stuid;
}
}
31.打印流(临靠IO流)
打印流很强大:
构造方法摘要 | |
---|---|
PrintStream(File file) 创建具有指定文件且不带自动行刷新的新打印流。 | |
PrintStream(File file, String csn) 创建具有指定文件名称和字符集且不带自动行刷新的新打印流。 | |
PrintStream(OutputStream out) 创建新的打印流。 | |
PrintStream(OutputStream out, boolean autoFlush) 创建新的打印流。 | |
PrintStream(OutputStream out, boolean autoFlush, String encoding) 创建新的打印流。 | |
PrintStream(String fileName) 创建具有指定文件名称且不带自动行刷新的新打印流。 | |
PrintStream(String fileName, String csn) 创建具有指定文件名称和字符集且不带自动行刷新的新打印流。 |
代码:
public class PrintStreamDemo01 {
public static void main(String[] args) throws Exception {
OutputStream os=new FileOutputStream("src/打印流/a.txt",true); //如果想要用追加数据管道,还是得用包装
PrintStream ps=new PrintStream("src/打印流/a.txt"); //可以直接通向文件
ps.println(97); //写入且换行
ps.println("你好世界");
ps.print(false);
ps.close();
}
}
注意:打印流不能被缓冲流包装,因为打印流功能很强大,粗俗的讲高级流只能包装低级流
打印流可以 方便,且高效的打印各种数据。
PrintStream不光可以打印数据,还可以写字节数据出去(write)
PrintWriter不光可以打印数据,还可以写字符数据出去
32.Properties类用法总结
Properties类的用法总结_源码复兴号的博客-CSDN博客_properties
Properties:属性集
Properties一般用于大型框架的底层,我们自己不怎么会用,但要明白原理。
Properties类继承了Hashtable,而HashTable又实现了Map接口,所以可对 Properties 对象应用 put 和 putAll 方法。但不建议使用这两个方法,因为它们允许调用者插入其键或值不是 String 的项。相反,应该使用 setProperty 方法。如果在“不安全”的 Properties 对象(即包含非 String 的键或值)上调用 store 或 save 方法,则该调用将失败。
33.网络通信
网络通信一定基于软件结构实现的:
1.C/S结构:客户端和服务器结构 常见程序有qq,迅雷,IDEA等
2.B/S结构:浏览器和服务器结构。 常见浏览器有谷歌,火狐,京东,淘宝等(开发中的重点,基于网页设计界面,界面效果可以更丰富:Java Web开发。
这两种架构各有优势,但是无论哪种架构,都离不开网络的支持。
网络编程,就是在一定的协议下,实现两台计算机的通信的技术。
网络通信的三要素
1.协议
计算机网络客户端与服务端通信必须事先约定和彼此遵守的通信规则。
如HTTP,、FTP、 TCP、 UDP、 SSH、 SMTP
2.IP地址
指互联网协议地址,俗称IP
IP地址用来给一个网络中的计算机设备做唯一的编号。
ipconfig指令查找自己的IP
ping ip值 可以检查本机与某个IP指定的机器是否联通,或者说是检测对方是否在线。
IPv4:4个字节,32位组成
局域网
城域网
广域网(公网):可以在任何地方访问
IPv6:128位 还未普及
注意:127.0.0.1=localhost是两个特殊的IP(localhost是域名,也算IP),它 本机IP地址。(不受环境影响,永远存在的IP值)
3.端口
端口号就是唯一标识设备中的进程(应用程序)。如果IP是地址,那端口就好像房间号
端口号:
用两个字节表示的整数:它的取值范围是0~65535
0~1023端口号一般用于系统网络服务和应用
所以我们普通的应用程序需要使用1024以上的端口号。
如果端口号被占用,会导致程序启动失败。
总结:用 协议+IP地址+端口号 三元组和 就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其他进程进行交互。
协议
传输层协议:
TCP/IP 协议: 传输控制协议
TCP协议是面向连接的安全的可靠的传输通信协议。
1.在通信之前必须确定对方在线并且连接成功才可以通信。(三次握手)
2.例如下载文件、浏览网页等(可靠传输)
UDP:用户数据报协议
UDP协议是面向无连接的不可靠的传输通信协议。
1.直接发消息给对方,不管对方是否在线,发消息也不需要确认。
2.无线(视频,通话),性能好,但会丢失数据!
InetAddress
该类代表ip地址,下面还有两个子类,分别是Inet4Address和Inet6Address他们分别代表IPV4地址和IPV6地址
该类没有构造器,可以通过它的两个静态方法来获取InetAddress的实例
getByName(String host) 根据主机名获取对应的InetAddress对象
public class NetDemo01 {
public static void main(String[] args) throws UnknownHostException {
//获取本机地址对象
InetAddress ip=InetAddress.getLocalHost();
System.out.println(ip.getHostName());
System.out.println(ip.getHostAddress());
//获取域名ip对象
InetAddress ip2=InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());
System.out.println(ip2.getHostAddress());
}
}
UPD通信 实现类
网络编程(DatagramSocket && DatagramPacket)_春林初绿的博客-CSDN博客_datagrampacket
TCP通信 实现类(重要)
TCP/IP协议的特点:
1.面向连接的协议
2.只能由客服端主动发送数据给服务器端,服务器端收到数据之后,可以给客服端响应数据。
3.通过三次握手建立连接,连接成功形成数据传输通道
4.通过四次握手断开连接
5.基于IO流进行数据传输
6.数据传输大小没有限制
7.因为面向连接的协议,速度慢,但是是可靠的。
TCP协议使用场景:
文件上传和下载
邮件传输和发送
远程登录
TCP协议相关的类
Socket 一个该类的对象就代表一个客户端程序
ServerSocket 一个该类的对象就代表一个服务器端程序
TCP通信也叫Socket网络编程。只要代码基于Sokcet开发,底层就是给予了可靠传输的TCP通信
Socket类构造方法
构造方法摘要 | |
---|---|
| Socket() 通过系统默认类型的 SocketImpl 创建未连接套接字 |
| Socket(InetAddress address, int port) 用于创建一个链接,向指定的IP地址上指定的端口的服务器端程序发送连接请求 |
| Socket(String host, int port) 同上,但该方法允许通过主机名字符串向服务器发送连接请求 |
Socket 常用方法
OutputStream | getOutputStream() 返回当前Socket的字节输出流 |
InputStream | getInputStream() 返回当前Socket的字节输入流 |
总结:
客服端的开发流程:
1.客服端要求请求于服务端的socket管道连接。
2.从socket管道通信中得到一个字节输出流。
3.通过字节输出流给服务端写出数据
服务端的开发流程:
1.注册端口
2.接受服务端的Socket管道连接
3.从socket通信管道中得到一个字节输入流
4.从字节输入流中读取客户端发来的数据
业务代码实现:
//ServerSocket类:
// 构造器: public ServerSocket(int port)
// 方法: public Socket accept(); --等待接收一个客户端的Socket管道连接请求,连接成功返回一个Sokcet对象
public class ServerDemo {
public static void main(String[] args) throws IOException {
System.out.println("=====服务端启动了=====");
//1.注册端口
ServerSocket ss=new ServerSocket(8888);
//定义一个循环来接受很多客户端的连接请求
while(true){
//2.开始等待接受服务端的Socket管道连接
Socket socket=ss.accept();
//每接收到一个客户端就为这个客户端管道分配一个独立的线程
new Thread(new ServerThread(socket)).start();
}
}
}
class ServerThread implements Runnable{
private Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//3.从socket通信管道中得到一个字节输入流 socket是通信管道,IO流才是用来传输数据的!!!
InputStream is=socket.getInputStream();
//4.把字节输入流转换成字符输入流
Reader isr=new InputStreamReader(is);
//5.把字符输入流包装成缓冲字符输入流
BufferedReader br=new BufferedReader(isr);
//6.按行读取数据
String message;
while((message=br.readLine())!=null){
System.out.println("收到了"+socket.getRemoteSocketAddress()+"的"+message);
}
}catch (Exception e){
System.out.println(socket.getRemoteSocketAddress()+"下线了");
}
}
}
public class ClientDemo {
public static void main(String[] args) throws IOException {
// 1.客服端要求请求于服务端的socket管道连接。
Socket socket=new Socket("192.168.10.1",8888);
// 2.从socket管道通信中得到一个字节输出流。
OutputStream os=socket.getOutputStream();
//3.将低级的字节输出流包装成高级的打印流
PrintStream ps=new PrintStream(os);
while(true){
//发消息
Scanner sc=new Scanner(System.in);
System.out.println("发送消息:");
ps.println(sc.nextLine()); //注意,服务端是按行读取数据,这里必须要用println换行
ps.flush();
}
}
}
=====服务端启动了=====
收到了/192.168.10.1:53812的你好啊
收到了/192.168.10.1:53818的吃了吗
/192.168.10.1:53818下线了
拓展:用线程池来实现上述代码!!!
//自己写的HandlerSocketThreadPool来包装ExecutorService线程池
public class HandlerSocketThreadPool {
//线程池
private ExecutorService executor;
//线程池:3个线程
public HandlerSocketThreadPool(int maxPoolSize,int queueSize){
this.executor=new ThreadPoolExecutor(
maxPoolSize, //线程池中核心线程数的最大值
10, //线程池中能拥有最多线程数
120L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(queueSize) //任务队列的个数 最多等待的个数
);
}
public void execute(Runnable task){
this.executor.execute(task); //把ExecutorService改成我们自己类的名字来用
}
}
//ServerSocket类:
// 构造器: public ServerSocket(int port)
// 方法: public Socket accept(); --等待接收一个客户端的Socket管道连接请求,连接成功返回一个Sokcet对象
public class ServerDemo {
public static void main(String[] args) throws IOException {
System.out.println("=====服务端启动了=====");
//1.注册端口
ServerSocket ss=new ServerSocket(8888);
//取一个线程池 可以有100个客户端连接,但是最多只能处理三个
HandlerSocketThreadPool handlerSocketThreadPool=new HandlerSocketThreadPool(3,100);
//定义一个循环来接受很多客户端的连接请求
while(true){
//2.开始等待接受服务端的Socket管道连接
Socket socket=ss.accept();
//用线程池
handlerSocketThreadPool.execute(new ServerThread(socket));
}
}
}
class ServerThread implements Runnable{
private Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//3.从socket通信管道中得到一个字节输入流 socket是通信管道,IO流才是用来传输数据的!!!
InputStream is=socket.getInputStream();
//4.把字节输入流转换成字符输入流
Reader isr=new InputStreamReader(is);
//5.把字符输入流包装成缓冲字符输入流
BufferedReader br=new BufferedReader(isr);
//6.按行读取数据
String message;
while((message=br.readLine())!=null){
System.out.println("收到了"+socket.getRemoteSocketAddress()+"的"+message);
}
}catch (Exception e){
System.out.println(socket.getRemoteSocketAddress()+"下线了");
}
}
}
客户端代码相同。
用线程池可以避免大量客户端创建线程造成死机。用了线程池可以规定最多只有三个客户端在和服务器交互,如果有客户端下线了,才处理其他正在排队的客户端。
基本的通信模型
1.BIO通信模式:同步阻塞通信(Socket网络编程)
同步阻塞通信模型是传统一请求一应答,即开启一个ServerSocket负责监听socket的连接。
同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
阻塞是指在没有数据的情况下,还要继续等待着读。非阻塞指在没有数据的情况下,会去执行其他操作,一旦有了数据再来获取。
BIO表示同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求的时候服务器端就需要启动一个线程进行处理,如果这个线程不做任何事情会造成不必要的线程开销,当然也可以通过线程池机制来改善。
同步阻塞式性能极差:大量线程,大量阻塞。
2.伪异步通信:引入了线程池
不需要一个客户端一个线程,可以实现1个线程复用来处理多个客户端!
这种架构,可以避免系统的死机,因为不会出现过多的线程导致线程死机。
但是在高并发下性能还是很差,因为线程数量少,数据依然是阻塞的,数据没有到来线程依然等待!!
3.NIO通信模式:同步非阻塞IO
NIO服务器实现模式为请求一个线程,即客户端发送的连接请求都会注册到多路复用器(相当于一个线程池)上,多路复用器轮询到连接有IO请求时才会启动一个线程来进行处理。
例如:有很多人(客户端)去办业务,前台会接收它们,安排到一个等待区,然后前台会轮着问他们是否要办业务,如果现在需要(发生了请求),就安排一个服务窗口(线程)来服务他们。
但它还是同步的,因为线程需要不断地接受客户端连接,去处理数据。
4.AIO通信模式:异步非阻塞IO
服务器实现模式为一个有效请求一个线程。客户端的IO请求都是由操作系统先完成IO操作后再通知服务器应用来启动线程进行处理的。
异步:服务器端线程收到了客户端管道以后就交给了底层操作系统来处理它的IO通信。自己可以去做其他事情。
非阻塞:底层也是客户端有数据才会处理,有了数据以后处理好通知服务器来启动线程进行处理。
有一个经典的举例。烧开水。
假设有这么一个场景,有一排水壶(客户)在烧水。
AIO的做法是,每个水壶上装一个开关,当水开了以后会提醒对应的线程去处理。
NIO的做法是,叫一个线程不停的循环观察每一个水壶,根据每个水壶当前的状态去处理。
BIO的做法是,叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。
可以看出AIO是最聪明省力,NIO相对省力,叫一个人就能看所有的壶,BIO最愚蠢,劳动力低下。
34.Junit单元测试
什么时单元测试?
单元测试是指程序员写的测试代码给自己类中的方法进行预期正确性的验证。
单元测试一旦写好测试代码,就可以一直使用,可以实现一定程度上的自动化测试。
单元测试一般要使用框架进行。
单元测试经典框架:Junit
Junit是什么?
Junit是Java语言编写的第三方单元测试框架。
单元测试的概念
单元:在Java中,一个类就是一个单元
单元测试:程序员用Junit编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试。
测试方法的要求:
1.必须public修饰
2.没有返回值没有参数
3.必须有注解@Test修饰
35.反射
反射,注解,代理,泛型是Java的高级技术,也是以后框架底层原理会经常用到的技术!
当你得到了类对象,你就可以解析里面所有的成分,因为所有的代码都在class里面。
public class ReflectDemo01 {
public static void main(String[] args) throws ClassNotFoundException {
//拿到类的Class文件对象
Class c1=Student.class;
//法二
Student swk=new Student();
Class c2=swk.getClass();
//右键类 copy reference就是它的全限名了
Class c3=Class.forName("反射.Student");
System.out.println(c3.toString()); //class 反射.Student
String name= c1.getSimpleName(); //获取类的简名
System.out.println(name); //Student
String allname= c1.getName(); //获取类的全限名
System.out.println(allname); // 反射.Student
}
}
获取类的构造器并初始化
public class Student {
String name;
int age;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
private Student() {
System.out.println("无参构造器被执行");
}
public Student(String name, int age) {
System.out.println("有参构造器被执行");
this.name = name;
this.age = age;
}
}
public class TestStudent02 {
//调用一个无参构造器得到一个类的对象返回
@Test
public void createObj01() throws Exception{
Class c=Student.class;
//获取无参构造器
Constructor constructor=c.getDeclaredConstructor();
//setAccessible()暴力反射,修改构造器权限,true为打开权限 打开权限为1次,只能在这里有权限
constructor.setAccessible(true);
//newInstance() 创建此 Class 对象所表示的类的一个新实例。
Student swk=(Student) constructor.newInstance();
System.out.println(swk); //无参构造器被执行 Student{name='null', age=0}
}
//调用一个有参构造器得到一个类的对象返回
@Test
public void createObj02() throws Exception{
Class c=Student.class;
//获取有参构造器
Constructor constructor=c.getDeclaredConstructor(String.class,int.class);
//newInstance() 创建此 Class 对象所表示的类的一个新实例。
Student swk=(Student) constructor.newInstance("小红",10); //有参构造器被执行 Student{name='小红', age=10}
System.out.println(swk);
}
}
public class Dog {
private String name;
private int age;
private static String school;
public static final String food="狗粮";
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public Dog() {
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class FieldDemo {
//获取所有的成员变量
@Test
public void getField(){
//获取类对象
Class c= Dog.class;
//拿到所有的成员变量
Field[] fields=c.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName()+"==========>"+field.getType());
}
}
//获取某个成员变量
@Test
public void getField2() throws NoSuchFieldException {
//获取类对象
Class c= Dog.class;
//拿到所有的成员变量
Field field=c.getDeclaredField("name");
System.out.println(field.getName()+"==========>"+field.getType());
}
@Test
public void setField() throws Exception {
//获取类对象
Class c= Dog.class;
//拿到所有的成员变量
Field field=c.getDeclaredField("name");
field.setAccessible(true);
Dog dog1=new Dog();
//参数一:被赋值的对象 参数二:该成员变量的值
field.set(dog1,"泰迪");
System.out.println(dog1.toString()); //Dog{name='泰迪', age=0}
//获取成员变量的值
String value=field.get(dog1)+"";
System.out.println(value); //泰迪
}
}
public class Cat {
private int age;
private String name;
public void run(){
System.out.println("我是run方法");
}
public void run(String name){
this.name=name;
System.out.println(name+"跑了起来");
}
private void eat(){
System.out.println("我是eat方法");
}
}
public class MethodDemo {
//获取类中所有成员方法
@Test
public void getDeclaredMethods(){
//a获取类对象
Class c=Cat.class;
//b获取全部声明方法
Method[] methods=c.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName()+"===>"
+method.getParameterCount() //方法参数个数 eat===>0==>void
+"==>"+method.getReturnType()); //方法返回类型 run===>0==>void run===>1==>void
}
}
//获取类中某个成员方法
@Test
public void getDeclaredMethod() throws Exception {
//a获取类对象
Class c=Cat.class;
//b获取无参的run方法
Method method=c.getDeclaredMethod("run");
//c触发方法执行
Cat cat=new Cat();
method.invoke(cat); //触发cat对象里的run方法执行 我是run方法
//获取有参的run方法
Method method1=c.getDeclaredMethod("run",String.class);
method1.invoke(cat,"小橘猫"); // 小橘猫跑了起来
}
}
那反射到底有什么用呢?
反射可以破坏面向对象的封装性。(暴力反射)
反射可以破坏泛型的约束性。
public class GenericReflect {
public static void main(String[] args) throws Exception {
//泛型是在编译阶段工作的,运行阶段泛型消失
//而反射是在运行时工作的
List<Double> scores=new ArrayList<>();
scores.add(99.3);
//报错 scores.add("小红");
Class c=scores.getClass();
Method add=c.getDeclaredMethod("add",Object.class);
add.invoke(scores,"小红");
System.out.println(scores); //添加成功
}
}
约束对于反射来说形同虚设!