2024/7/14
java语言的特点
跨平台性:一次编译,到处执行。原理:编译后得到的class字节码文件可以再不同平台的java虚拟机JVM上转为该系统适配的指令。
JDK的安装与环境变量的配置:JAVA_HOME为所安装的jdk中的bin文件夹所在的目录。
Path则为bin目录。
java项目的创建:首先在project下创建源文件夹,然后再src中创建包package,最后在package中创建类。
类结构: class 类名{} ,一个java文件中只能有一个public外部类,且这个类的名称必须与文件名相同。
java的三种注释:
1.单行知识 //
2.多行注释 /*
*
*/
3.文章注释 /**
*
*/
标识符:起到标识的作用,方便日后使用。包括常量名、包名、类名、方法名、参数名等
关键字:开发语言中已经内定的一些单词,并且赋予了特殊的作用,开发人员只能按照指定的语法进行使用,在java中 关键字的所有字母都是小写
标识符的命名规范:
1.标识符只能由数字、字母、下划线_和$(写中文不会报错,但行业内不允许)
2.标识符首字母不能是数字 比如 int 22num; 这是不允许的
3.标识符不能是关键字(可以包含关键字,比如 int static12;)
关键字:开发语言中已经内定的一些单词,并且赋予了特殊的作用,开发人员只能按照指定的语法进行使用,在java中 关键字的所有字母都是小写
以上三条是硬性规范
以下为行业内规范
4.类名class首字母大写,包名package全部小写(若包名为类似com.easy,则有两个文件夹,一个是com文件夹,另一个是easy文件夹在com文件夹中)
5.常量名全部大写
6.其他首字母小写,使用驼峰命名法。如:maxNum
7.所有标识符尽量达到见名知意
常量与变量:
用于储存数据, 程序运行阶段可能发生变化的量,叫做变量 程序运行阶段不能发生变化的量,叫做常量
常量的声明 final 类型 变量名; ,如 final int MAX_NUM;(常用下划线来区分单词)
变量的声明 类型 变量名 ;,如int num;
变量的第一次赋值成为初始化,想使用变量必须先初始化,未初始化的变量不可使用
同一个作用域下不可以声明同一个变量 如 int a; char a;
一个{}为一个作用域
数据类型:
基本数据类型 和 引用类型
不同的数据类型用于存储不同的数据形式
八个基本数据类型:整数类型 byte short int long 浮点类型 float double 字符类型 char 布尔类型 boolean
byte 1个字节 8个二进制位 -2^7~x^7-1 -128~127 负数计算 反码再加一(二进制上加一)
short 2个字节 16个二进制位 -2^15~2^15-1
int 4个字节 32个二进制位 -2^31~2^31-1 整数类型的默认类型
long 8个字节 64个二进制位 -2^63~2^63-1 一般用于表示时间
float 单精度浮点型 4个字节 默认值0.0f
double 双精度浮点型 8个字节 默认类型 默认值0.0
字符类型 char 两个字节 取值范围0~65535
布尔类型 一个字节或四个字节
boolean t = true; //一个字节
boolean[] b_arry={true,false};//四个字节 在数组里面当成整型
布尔类型默认值为flase 布尔类型不能和任意类型进行转换
byte byteNum = 12; 不能超出取值范围 ,如byteNum=128;则会报错
不能直接用float给变量赋值
1.强制类型转换
2.使用f标识 (在后面加个f)
float floatNum = 12.22f;
floatNum = (float)12.22;
float类型的取值范围比int类型大 double类型取值范围比long类型大
double和float是需要尾数和指数运算出来的,所以浮点型不能十分精确的表示一个小数
在表示价格、金融、税率有关金融方面的数据,不使用浮点型
java基本数据类型间的转换
java中的char是两个字节表示一个字符 0~65535(FFFF)
三种表示方法
1.utf-8 unicode表示方法
c='\uffff';
2. c=65535;
3.c='a';
数据类型间的转换:
隐式转换:程序会自动完成的转换,从取值范围小的数据转化为取值范围大的数据
强制类型转换:从取值范围大的转为取值范围小的(一刀切),可能会出现有效位的缺失,导致出现错误
布尔类型不能与其他类型转换,强制类型转换也不可以
\转义符
numChar='\r'; //回车
numChar='\n'; //换行
不同进制表示方法:
a=0b111;二进制 //字母大小写都可以
a=0B111;
a=0111;//八进制
a=0x111;//十六进制
输入输出:
System.out.println();//可以不带参数,只输出一个换行
System.out.print(12);//必须要带参数
输入流: Scanner scan = new Scanner(System.in);//声明一个扫描器 最后需要关闭流 不然服务器会当机
int a = scan.nextInt();//等待输入一个整数,然后赋给a
scan.close();//关闭流
System.in 输入流对象
编写一个测试类在主方法中 main方法是java程序的入口
算数运算符与逻辑运算符
算数运算符 + - * / %
整数和整数运算 得到的结果是整数int
除非有long类型参与,得到的结果就是long类型
若有小数参与,得到的结果就是double型
%运算的结果正负 A%B得到的结果的正负号和A相同
一元运算符 a++ a-- ++a --a 自运算符
c=a++; //后++ ,现将a的值赋给表达式,a的值在加一
c=++a; //前++ ,现将a的值加一,再将a的值a的值赋给表达式
比较运算符 < > <= >= == != 得到的结果是布尔类型的
== 在基本数据类型中比较的值是否一样 和类型无关
逻辑运算符 运算布尔值
&&逻辑与运算符 ||逻辑或运算符 !逻辑非运算符
逻辑运算符中的短路现象:
多个表达式运算,前面的表达式已经可以明确整个表达式的结果,后面的就不需要再运行
如:
int a =12;
int b =0;
result = a++<0&& b++>0; //后面的b++被短路掉
a=0;
b=0;
result = a++>=0 || b++>0;//b++被短路
运算符中优先级最高的是(),最低的是赋值=
2014/7/15
运算符: 二进制运算符 位运算符
按位与运算: & A&B 将A和B转化为二进制数,右侧对齐,上下进行比较,两者都为1,结果即为1,否则为0
按位或运算: | A|B 将A和B转化为二进制数,右侧对齐,上下进行比较,两者有一个为1,结果即为0,都为0时,都为0时,结果才为0
按位异或运算: ^ A^B 将A和B转化为二进制数,右侧对齐,上下进行比较,两者不同则为1,相同则为0
反码运算 ~ ~A 整数的反码是本身,负数的反码是将符号位以外的都取反
移位运算 << 向左移位,放大2的n次方倍 最快的乘法运算方式
>>向右移位 左侧空出来的补上符号位
>>>向右移位,左侧空出来的全部补0
& | 也可对布尔值进行运算,得到布尔值
& 和 && 的运算结果是一样的,但运算过程完全不同
bool = a++<12 & b++<12; //移位运算符都要运算完毕 a和b都转化为二进制再进行运算 bool = a++<12 && b++<12;//逻辑运算符可能有短路现象
赋值运算中 优先级最低的 是 = += -= *= /= %= &=
两个变量交换值得三种方式
1.第三变量法
int a = 10;
int b = 20;
int c = a;
a=b;
b=c;
2.加法交换
int a =20;
int b =10;
a=a+b;
b=a-b;
a=a-b;
3.异或运算
int a = 15;
int b = 20;
a=a^b;
b=a^b;
a=a^b;
三元运算:A?B:C; A是布尔类型,若为true则取B的值,否则取C的值 赋给表达式,再付给前面的变量
int result = 12>34?11:33; //result为33
注意System.out.println(true?12:11.0); //此时输出12.0,因为有double浮点型11.0参与
流程控制 if switch
if(){}
else if(){}
else{}
当设计数的区间是,可从范围小的开始,比如成绩评分,先判断是否>90,依次往下是否>80....
这样可以省略掉如:=<90 && >80 中的左侧的判断部分
switch(){
case 1:
break;
case 2:
break;
.....
default:
}
匹配到某一个变量的值是什么,如果匹配到某一个case项,就会从这个项开始执行
运行到break或代码块结束
若没有break,就会继续它下面的其他的case项,直到代码块结束,所以切记不要忘记break
所有case项都没有成功匹配才会执行default(若default放在第一个且没有break,那在回归头来执行完最上面的default后会依次把下面的case都执行,直至代码块结束)
注意:
1.default和case项的顺序可以是错乱的,但是顺序不会乱
2.switch可以匹配的类型 byte short int char String enum(枚举)
循环 while 和 for
for适用于已知循环次数的情况
不知道循环多少次时用while
while(循环的条件){ 循环体 }
char和int 是可以互相转换的
eg:输出a~f
int i = 'a';
while(i=<'f'){
System.out.println(char(i));
i++;
}
do{}while(); 与while(){}的区别
前者有‘;’结尾,且不论条件是否成立都会执行一次do
死循环与无限循环
死循环:没有循环结束条件的循环,死循环下面不可写任何代码,语法上不允许,因为程序会认为循环无法结束
无限循环:有循环结束的条件,但永远无法达到,
//while(true){} 死循环 //a=0; //while(a<100){a--;}无限循环
Math.random();// 获取[0~1)之间的随机数
可以将它乘以某个数在强转为int型
如 int i; i=(int)(Math.random()*80);
break continue 流程控制语句
break 结束所在的循环体
continue 跳出当前循环,去执行下一次循环
跳出多重循环
1.可使用break跳出多重循环,只需在代码块前写一个lable,再break lable;
如下:
a:for(;;){ for(;;) { for (int bb=0;bb<100 ;bb++ ) { if(bb==50) { break a; } } } }
2.用一个bool型的锁来控制整个循环 如下:
boolean bool = true; for(;bool ;){ for(;bool ;) { for (int bb=0;bool && bb<100 ;bb++ ) { if(bb==50) { bool = false; } } } }
数组
数组的定义
int[] arr; //声明一个int型的数组arr
int[] arr = new int[]{1,2,3,4};
int[] arr = {1,2,3,4};
int[] arr = new int[4];
//数组的限定 //1.只能存放指定类型的数据 //2.数组的长度是不可变的
//使用数组中的元素需要下标 从0开始 依次增加 System.out.println(arry[3]);
System.out.println(Arrays.toString(arry));//Arrays工具的此方法能将数组转为字符串输出 System.out.println(arry);//此时会打印首个的地址 [I@776ec8df
数组的长度是该数组的属性 可以直接 arr.length 调用
//打印出arry数组里的所有元素 int i=0; while(i<arry.length){ System.out.println(arry[i]); i++; }
二维数组:
要打印出二维数组中的所有元素 可使用 Arrays.deepToString
如 System.out.println(Arrays.deepToString(arrys));
二维数组的限定:二维数组中,第一位的个数不能改变
如 a[5][3] 其中的5是不可改变的 而深度3可以改变
数组越界:为数组赋值的个数超过了数组定义的长度
Index 5 out of bounds for length 4
2024/7/16
方法的定义 :
访问权限 返回值类型 方法名(参数列表){方法体} 如:public static int sum(int a,int b){}
返回值类型:该方法必须返回一个这个类型的对象
当一个方法不需要返回值,返回类型就定义为void
void中不能写 return null; return null;是返回空,也算返回,所以不能写
但是 void 可以单写 return; 代码执行到return 就结束了,下面就算有东西也不会执 行
形参和实参
形参:定义方法时,参数列表的参数名就是形参
实参:调用方法是,实际传入的参数就是实参
重载
在一个类中,方法名相同,参数不同即为重载
不能定义方法名和参数列表都相同,但返回类型不同的方法
三种参数不同:1.参数类型不同 2.参数个数不同 3.参数(不同种类)顺序不同
eg:public static void sum(int a)
public static void sum(byte a)
public static void sum(int a,int b)
public static void sum(int a,double b)
public static void sum(double a,int b)
可变参数:类型后面跟三个点
声明:sum(int ... a);
使用:将可变参数当数组使用 int... a; for(int item:a);'
注意点:1.一个方法中最多有一个可变参数
2.可变参数必须在参数列表的最后一个
3.可变参数数量可变,类型不可变
4.调用可变参数的方法,其参数部分可以用数组代替可变参数
对于方法public static int sum(int ... a){}
sum(1,2,3,4,5,6,7,8,9); sum(arry);
5.可变参数可以不传(传入数量为0)
sum();
递归:方法调用自身 需要有结束条件
求阶乘的递归代码如下:
public static int jiecheng(int num){ if(num==1){ return 1; //结束条件 } return num*jiecheng(num-1); }
排序算法之冒泡排序:
数组中前后两个相邻元素进行比较,如果前面的大,就交换位置
不要数组越界
public static void maopaopaixu(int[] arr) { for (int j=0;j<arr.length-1;j++) { for (int i = 0; i < arr.length -j- 1; i++) { //前后两个相邻元素比较 如果前面的大 就交换位置 //数组越界的可能性 i到倒数第二个就可以了 //比较了一趟 最大值到了数组最后 if (arr[i] > arr[i + 1]) { int temp = arr[i]; arr[i] = arr[i + 1]; arr[i + 1] = temp; } } } }
类
类的定义:具有相同特征或行为的多种个体的统合(我解释的)
类就是具备某些共同特征的实体的集合,它是一种抽象的数据类型,它是对所具有相同特征实体的抽象(百度解释的)
类的属性:类中可以定义属性,所定义的属性也叫作全局变量,在整个类中都可以访问到的量
类的方法:在类中可以定义方法,表示类的某种行为
实例化对象:已定义了某个类,创建该类型的特定对象即为实例化对象
比如 Staff sf= new Staff(); sf就是员工类的实例化对象
调用属性和方法 一般直接 实例化对象.属性 或 实例化对象.方法
如 sf.age; sf.work();
构造方法(构造方法没有返回值类型):
ps:一个类中只能有一个public外部类,且类名要与文件名相同
如果在一个类中没有自己创建构造方法,系统会自动创建一个空的构造方法
如 Staff(){}
定义构造方法:1. 没有返回值类型 2.方法名和类名相同
3.如果一个类没有定义任何的构造方法,系统会给与一个默认的空的构造方法
4.类中一旦自定义了任意构造方法,系统给定的默认构造方法就消失了
构造方法可以重载 ,比如有空的构造方法 和 有形参的构造方法 有形参的构造方法可以顺便为构造的实例化对象中的属性赋值
System.arraycopy(values,0,result,0,size); 示例方法可以将values数组中的从第0个开始的总共size个元素 复制到result数组中从0个开始的size个格子中
面向对象的三大特征 :继承 封装 多态
封装:把对象的属性和操作结合为一个独立的整体,将对象的内部实现细节隐藏起来
继承:关键字 entends 如public class A extends B{ } A类便继承了B类,A类是B类的子类,B类是A类的父类
一个类只能继承一个父类,子类就具有父类中定义好的属性和方法
不是所有的属性和方法都能调用到,只能调用父类中的public protected 和本包下的父类的default
继承实现了代码重用,是实现多态的基础
注意:一个类只能有一个父类,java支持多重继承
方法重写:子类对父类中继承过来的方法重新定义,这种方式叫做方法的重写
返回值类型 方法名 参数列表都与父类中继承来的方法相同,只有方法的实现不同
可以在重写的方法前面前面写@Override注解来验证是否是方法重写
而且重写的方法的访问权限只能更开放,不能更闭塞
eg:父类中的protected方法,子类重写时不能设为private ,可以设为protected和public
this和super关键字:
局部变量:在方法体或者代码块中声明的变量 在类当中定义的变量就是全局变量 在局部变量和全局变量重名的情况下,可以使用this关键字标注全局变量
比如set和get方法
public String getSex() { return this.sex; } public void setSex(String sex) { this.sex = sex; }
super
关键字是Java中用于指代父类对象的一个特殊引用。
可以使用super
关键字来访问父类的成员变量、方法和构造函数。简单来说,super
关键字可以理解为“父类对象”的代称。
自我理解:this指向当前类中声明的属性(全局变量)和方法,super指向父类中的属性和方法
子父类间的构造方法:
父类构造方法:
public Plane(String code,String color) { this.code = code; this.color= color; }
子类构造方法:
public BigPlane(String code,String color){ //子类的构造方法中,首行必须调用父类的构造方法 //默认调用父类的无参构造方法 //如果父类中没有无参构造方法,子类构造方法中必须明文声明调用父类哪一个构造方法 // 使用super关键字调用父类的构造方法 super(code, color); }
向上转型:
形式如下: Parent p=new Child();
对于上转型p 调用方法看对象Child中的方法,调用属性则看Parent中的声明
多态
静态多态 动态多态
静态多态主要由方法的重载造成,在类编译是可以确定调用的具体是哪一方法
动态多态主要由子类对父类的方法重写造成,只有在执行到改行代码才能确定执行的是哪个类中的方法
如下代码中创建了一个Parent类的p,这个p通过getObj1方法可以指向任意子类, 而它指向不同的子类就会调用不同的方法p.method() public static void main(String[] args) { int num=1; Parent p = getObj1(num);//p这个变量可能指向任意子类的对象 p.method();//多态调用方法时 可能会出现多种结果 //多态分类:静态多态 动态多态 //静态多态主要是因为重载造成的,在类编译时就可以确定调用的具体是哪一个方法 //动态多态主要由重写造成 只有在执行到改行代码才能确定执行的是哪个类中的方法 } public static Parent getObj1(int a){ if(a==1){ return new SonA(); }else if(a==2){ return new SonB(); }else{ return new Parent(); } }
2024/7/17
抽象类与抽象方法 abstract
使用abstract修饰的类是抽象类 :抽象类没有直接实例,不能new,抽象类用于被子类继承
抽象类中可以定义抽象方法 抽象方法也用abstract修饰,没有方法体,用于被子类重写
public abstract void methodB(int a,double b);
实体类要继承抽象类就必须实现抽象类中的抽象方法,可用@Override检查是否重写成功
抽象类继承抽象类时可以不实现父亲抽象类中的抽象方法
抽象类中的static方法不能被重写 static和abstract是互斥的
static修饰的方法是属于类本身的,不能被重写,子类写的也是属于子类自己的,而不是从父类重写来的
抽象类中可以有构造方法 ,用于被子类使用super调用
public abstract class EasyAbstract { EasyAbstract(){} int a; EasyAbstract(int a){//构造方法 用于被子类使用super调用 this.a=a; } }
lass Son extends EasyAbstract{ int a; Son(int a){ super(a); } }
接口Interface
接口中只能定义方法,但是没有方法体
接口不能定义构造方法
接口中的方法叫做抽象方法,没有具体的实现的方法
接口当中定义属性 接口当中定义的属性都是常量 默认会用 public static final修饰
public static final String MAX_SPEED="20000";//属于类的 且不能被重新赋值 为常量 String Min_Speed="90";//也为常量 不写修饰 默认为 public static final
接口中定义的方法(抽象方法)默认使用public abstract修饰 为了被重写
接口当中可以定义default修饰的实体方法,虽然使用的修饰是default,但访问权限是public
default void test(){ }
java中使用implements声明一个接口
类实现接口必须实现接口中的 抽象方法
接口不能继承类
抽象类可以继承接口
一个类实现多个接口
接口可以继承接口,使用extends 一个接口可以实现多个接口 用逗号隔开
如果接口中只有一个未实现的方法(抽象方法),这个接口称为函数式接口,可以使用
@FunctionalInterface来验证
interface IVehicle { public static final String MAX_SPEED="20000";//属于类的 且不能被重新赋值 为常量 String Min_Speed="90";//也为常量 不写修饰 默认为 public static final //接口中定义的方法默认使用public abstract 修饰 为了被重写 void transport(); default void test(){ } } class ObjectInter implements IVehicle{ public void transport(){ //不能少了public 因为接口中的方法默认是public 的 ,重写不能比public'更狭隘 } }
Object类
java是面对对象的,在java中所有的引用类,默认继承了Object
所有引用类型的默认值是null 包括封装类 在引用类型中 ==比较的是地址是否是同一个
Object obj=new EasyOnbect();
Object objA = obj;
Object objB = new EasyOnbect();
objB=null;//回收 让 objB指向的new EasyOnbect();没人用了
objA=null;//此时 还有obj跟他指向原来指向的同一个对象,所以回收后不会调用finalize
System.gc();//会使用自己定义的finalize()方法 //finalize是对象执行的最后一个代码 (空间要被销毁的时候)
包:
包名在首行
使用本包下的类不需要导入包,使用其他包下的类需要导入包
所有的类默认引入java.lang包
import java.util.*;//代表导入java.util下所有包
类名重名的情况下可以使用类的全名指定具体使用哪个类 like:new com.easyb.EasyA();
包没有父子关系
包装类
java是面向对象的,所有的内存都是对象,为了实现万物皆对象,java中给每一个基本数据类型,提供了对应的封装类型
封装类型的默认值为null
//基本数据类型的封装类型(下面为封装类型) // byte short int long // Byte Short Integer Long // float double //Float Double // char //Character //boolean //Boolean
基本数据类型的封装类,可以直接和基本数据类型直接转换
基本数据类型转化为封装类型(引用类型),便有了方法和属性
装箱:基本数据类型转化为对应封装类型的过程
拆箱:将封装类型转化为基本数据类型的过程
基本数据类型的封装类型的缓存
Byte Short Integer Long 的缓存范围:一个byte的大小 -128~127
只有Integer类型的缓存范围可以调整
浮点型的封装类型没有缓存
Charater的缓存范围:0~127
Byte V = (Byte)intB; 转不了,封装类型没有直接转换
int iN=1200; Short sN=1200;
基本数据类型与封装类型比较 封装类型拆箱之后在比较(比较的值)
Double dN=1200.0;//不能写1200因为是封装类型,没有隐式转换,类型要相匹配
System.out.println(sN==dN);//报错 因为两个不同的封装类型不能进行比较
new Byte("12"); new Short("12"); new Double("12");new Double("12.0"); new Character('a'); Integer.valueOf("18");//valueOF 和 parseInt都可以将字符串 转为Integer类型 Integer.parseInt("12");//valueOf返回Integer类型 parseInt返回int类型
static关键字
修饰的属性是属于类的 可以使用类名直接调用static修饰的属性和方法 //静态属性对所有的对象(本类的)是共享的 //本类的对象也可以调用静态的属性和方法,调用方式还是静态方式 //静态方法不能直接调用非静态的属性和方法 (存在未创建成员的情况,此情况下没有对象的成员属性让静态方法调用) //静态方法可以调用其他静态方法 成员方法可以直接使用静态属性或方法
fianl关键字
1.final可以修饰类 不能被继承的类
2.final可以修饰方法 不能被重写的方法
3.fianl可以修饰量 fianl修饰的量不能被重新赋值 =
fianl修饰的成员变量有两种方法初始化
1.直接赋值初始化 2.在构造方法中实现初始化
final int height=8;//直接赋值 final int length; public EasyFinal(){ length=100; }
final int[] a={1,2,3}; // a=new int[]; 不可以 ,因为 final 修饰 a 不能被重新赋值 a[2]=1; //但里面的内容没被修饰,可以重新赋值
深浅拷贝
浅拷贝只拷贝 自身的内存,不拷贝关联的属性 原来关联着谁 拷贝出来后仍关联谁 深拷贝会连同关联的属性也赋值一份作为自己独有的,原来的对象属性修改就与它无关了
被克隆的对象的类必须Cloneable (必须实现Cloneable接口)
2024/7/18
字符串类型 String类 属于引用类型 默认值为null
声明一个字符串的方法:String str="123";
String str=new String("456");
字符串类型类似于字符的数组类型
char[] arr = {'a', 'b', 'c', 97}; 将97代表的字符存入数组
字符串的拼接:使用 加号‘+’拼接
‘+’ 在字符串拼接和 数学运算时的优先级是一样的
ystem.out.println("123" + 123 + 123);//123123123 先转化为了字符串 后面就也是字符串 System.out.println(123 + 123 + "123");//246123 先相加得到246在跟123拼接成字符串
字符串和所有类型相加拼接得到的都是字符串 如 str = "123" + 23; //12323
str = "123" + new Object();//"123"+这个对象的toString方法的结果 System.out.println(str); str = "123" + new int[]{1, 2, 3}; //数组是引用类型 也是打印toString方法的结果 数组中ToString方法的结果是首个字符的地址 System.out.println(str);//123[I@4eec7777
字符串的比较 使用equals方法比较字符串 1.看是否为同一个 2.看类型是否相同 3.看内容是否相同
bool=new String("123").equals(new String("123"));//true
若使用"=="则是false,因为双等号在引用类型中比较的是地址,新建的两个对象都是指向不同的地址的
String类型里面的常用方法:
一、valueOf()
str = String.valueOf(12); valueOf将括号里面的12(shi er)转为了字符串类型的12(yi er)赋给str
二、indexOf()
查找子串出现的位置(index),找不到返回-1;
int index="123456".indexOf("34"); 返回2
三、lastIndexOf()
找出子串最后出现的下标
index="123123123".lastIndexOf("1");//找最后出现的下标 6
四、charAt()
获取指定位置的字符
char item="123456".charAt(4); //'5'
五、substring()
截取字符串
String str1="123456".substring(1); //23456 从第一个下表开始的字符串
String str2="123456".substring(1,4);//包含开始下标,不包含结束下标 234
六.replace()
字符串替换
str="12345634".replace("34","aaa");//12aa56aaa
str="12.345.634".replaceAll(".","a");//replaceAll中'.'代表任意字符
结果为//aaaaaaaaaa
七、split()
分割字符串
String[] strArr="123123123".split("2");//[1, 31, 31, 3]
//若所选的分隔regex在最前面,得到的结果会在最前面有空串,在最后的话就不管了 String[] strArr1="1231231231".split("1");//[, 23, 23, 23]
八、length()
获取字符串长度
int l = "123456".length();
九、trim()
去除前后空白位 空格 \n \r \t \f
strB="\n\r1 23 \t3 \n \r "; 结果为:1 23 3 System.out.println(strB.trim());//字符之间的空白位不会被去掉
十、toUpperCase()和toLowerCase()
转换大小写
str="123abc".toUpperCase();//只有字母转成大写 123ABC
str="123ABCabc".toLowerCase();//将大写字母转成小写 123abcabc
十一、isEmpty
判断是否为空串
boolean bo="123".isEmpty();//是空串返回true 否则返回false
字符串常量池
String对象 定义以后不可改变 定义的量为常量
String对象里的字符是存在它的内部 value数组中,不可被更改
value数组用private finall修饰
字符串常量池 池:容器 作用:便于重用
字符串怎样加入到常量池中 ------使用量声明的字符串会加入到常量池中(双引号直接引起来的字符串就是量的形式,会加入到常量池中)
like:String str="abc"; Integer.valueOf("23");
char[] arr={'a','b','c','d'}; //不是量的形式
String strA="123"; String strB="123";//上面的放到常量池中,可以直接重用 地址相同(指向同一个地址)
程序中第一次使用量的形式定义"123"字符串,会将这个字符串传入字符串常量池中
之后再使用量的形式使用该对象就直接 使用常量池里面的对象
如上的strB就指向strA指向的地址
如果是String strC = new String("123");
那么strC就不指向strA指向的地址,而是new出来一个新的地址来放"123"
String strC = new String("123");
String strE="12"+"3";//"123"
String strF="1"+"2"+"3";//"123"
在实例化strC之后,再实例的strE和strF都跟strC指向同一个地址,不跟strA是因为就近原则
int item="12"; String strG=item+"3";此处的的strG不跟strC指向同一个地址,因为有变量item参与,变量是不确定的,要运行完成后才能确定,在解析的时候不能确定,因此会新建一个
以上用到了常量优化机制:给一个变量赋值,如果等号右边没有一个变量,那么就会在编译阶段计算该表达式的结果。
itern方法 str.itern();会返回str对象在字符串常量池中的副本对象
如果str在常量池中没有副本对象,就复制一个放到常量池中
过程:检查字符串类型str是否在字符串常量池中存在副本,如果不存在就复制一份存入常量池中,然后返回常量池中的副本对象,如果已经存在副本对象,就直接返回副本对象
如果两个字符串类型变量的内容相同(是equals的),那么他俩的intern()是指向同一个地址的
//new String("abc"); 可能创建了一个 或 两个 对象 //如果有副本,就直接创建一个对象 指向 池中的副本 //如果没有副本,就创建一个对象,再复制一个放入字符串常量池中
StringBuilder 和 StringBuffer
String str=""; for(int i=0;i<10;i++){ str=str+i; //会产生很多中间串 0 01 012等占用存储 }
为了在拼接字符串时不产生中间串 可以使用StringBuffer和StringBuilder来声明对象,然后使用当中的append方法,如下:
StringBuilder strB=new StringBuilder(); strB.append("123"); strB.append("abc"); System.out.println(strB.toString());//123abc
使用append方法往StringBuilder数组中追加字符(StrringBuilder中有个方法会在追加字符时将原来的字符数组赋值给另一个空间更大的数组才存储 追加后的 字符数组,因此过程中没有产生字符串对象)
StringBuilder的默认容量是16,也可自己设置 ,在空间不够时,会扩容 至16*2+2,下次继续*2+2;
即 :追加字符时,容量不够就需要扩容,默认为原来的 容量*2+2
StringBuffer与StringBuilder的区别
StringBuffer是线程安全的,但效率会降低,在单线程中没有区别,在多线程中效率会比较低
数组复制方法System.arraycopy(src,0,dest,0,23); 数组复制 参数介绍:从src数组的第0位 复制 到dest数组的第0位,总共复制23位。
时间类型Data
Date date = new Date();//获取当前的时间(运行时的)
long time=date.getTime();获取从1970-1-1 00:00:00 000开始计时至今的数
time=time+2*24*60*60*1000;后面为所加的时间 两天*24小时*60分钟*60秒*1000毫秒 即两天的毫秒数
date.getMonth();//0~11 代表月份 同理 0~59代表分钟 和 秒数 0~23代表小时
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS"); String sdfSte=sdf.format(date);以指定形式(yyyy-MM-dd HH:mm:ss SSS)输出日期
ZonedDateTime zdt = ZonedDateTime.now(); Object obj=zdt.getZone();//返回一个时区的值
真随机与伪随机
double ran = Math.random(); 调用该方法生成一个[0-1)随机数,赋给ran 这是真随机的
若要获取[8-90) 可以Math.random()*82+8 (乘范围区间大小 加起始的值)
随机数对象(伪随机)Random
Random ranObjA=new Random(12);
Random ranObjB=new Random(12);里面传入的单个参数为种子,
int a=ranObjA.nextInt(); //ranObjA和ranObjB的种子的值一样,同次数随机获得的数是一样的 不传种子默认时间戳为种子 int b=ranObjB.nextInt();
a=ranObjA.nextInt(200);//来这里面的参数200 即为限定生成的随机数在200以内 b=ranObjB.nextInt(200);
取整方法
四舍五入:取靠近的 比如-1.5 四舍五入后为-1
long num=Math.round(12.5);//13 double类型返回long int num1=Math.round(12.33f);//12 float类型返回int
向上取整
double ranNum=12.5;
double ceilNum=Math.ceil(ranNum);//13.0;
向下取整
double floorNum=Math.floor(ranNum)//12.0;
2024/7/19
容器 集合 ==>数组集合 和 链式集合
数组集合 接口 List 实例化类型ArrayList类型
List list = new ArrayList();
此后list可调用ArrayList重写的方法
1. list.add("A"); 添加元素 也可以放入数组 list.add(arr); 因为该list使用的Object类型的数组 万物皆对象 皆可存入Object数组中
2.获取元素 Object obj = list.get(2); System.out.println(obj);
3.在指定位置插入元素
list.add(1, 44);//再下表为一的地方插入44,后面的往后挪,可以插入到最后一位,但不能再往后插入,因为越界时中间有空出来的位置,那个位置就是不确定的,这样不可以
4.替换
list.set(2, 22); //下表为2的元素设置为22
5.查看是否包含某一元素
list.contains();返回一个bool值
boolean bool = list.contains(22);也可用indexOf方法
bool = list.indexOf(22) != -1;
6.查看是否包含一个集合里的所有元素
bool = list.containsAll(listA);用containsAll()方法查看list数组中是否包含listA数组中的所有元素
7.把一个集合的元素都添加到另一集合
list.addAll(listA);//依次从后面添加
list.add(listA);//将listA作为一个元素添加到list中//[A, 44, 22, null, 33.33, n, [33.33, null, 2]]
8.删除元素
list.remove("A");//只会删除找到的第一个元素
list.remove(2);//按照下标删除
list.remove((Integer) 22);删除整数封装类型的22
remove删除下表位置的对象时会返回对象,越界时会报异常,
remove删除按照内容删除时会返回一个bool值
9数组的大小 list.size()
依次打印元素
1.for循环 for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }
2.each方法 for (Object item : list) {//each方法 System.out.println(item); }
3.Iterator迭代器
Iterator it = list.iterator();//获取一个迭代器 //不知道有多少个,迭代器只知道下一个元素是谁 和 有没有下一个元素 while (it.hasNext()) { System.out.println(it.next()); }
ArrayList默认容量是10,默认扩容至1.5倍
LinkedList 与ArrayList用法相同 结构(存储数据的方式)不同
ArrayList用数组存储 LinkList用链表存储 最大值Integer.MAX_VALUE
ArrayList是通过数组存储数据的 通过下标查找数据比较快 LinkedList是链表结构,查找数据需要一个一个往后找,查找速度较慢 ArrayList 插入和删除慢 (需要前后移) LinkList插入和删除块一些 因为LinkList查找较慢,所以通过双向检索优化检索速度
小知识:
//数组的初始化 直接赋值 是 静态 先设置长度 再赋值 是 动态 //用C语言写的 的代码 是 原生的 本地的 java代码不可以改变这些方法 用native修饰 //构造方法不可以继承 ,但可以通过super指定调用父类的构造方法 //两个变量指向的对象 equals 是相等的,那么hashCode得到的数值就相等 //String类用fina修饰,没有子类
代码块 {}
类中的代码块{}为成员代码块,每次new对象的时候执行,在构造方法之前执行
static修饰的代码块为静态代码块,一个静态代码块在程序的执行期间只会执行一次,在加载类对象的时候执行,在成员代码块之前执行
在执行一个子类时,如果子类和父类都有静态代码块,会先执行父类的
执行顺序
//1.父类的静态代码块 //2.子类的静态代码块 //3.父类的成员代码块 //4.父类的构造方法 //5.子类的成员代码块 //6.子类的构造方法
内部类
在类外定义的类叫做外部类,在类内定义的类叫做内部类
外部类只能由default和public修饰
静态内部类:使用static修饰的 可以用protected和private修饰
成员内部类:没用static修饰,只能由所在类的对象调用 ,可以在所在类中调用,public protected default private都可用来修饰成员内部类
局部内部类:定义在方法当中,只能在方法中调用 很少用到,不能用public修饰,因为它出不去方法
匿名内部类:先写一个抽象类
abstract class AbstractClass{ public abstract void method(); }
声明该抽象类 时 直接重写抽象方法,使之指向(实例化)一个实现了抽象方法的,没写名称的类
如下:
AbstractClass ac=new AbstractClass() { @Override public void method() { } };
可以用getClass方法获得该匿名类的名称
System.out.println(ac.getClass());//class com.easy719.EasyInnerClass$1 代表这个类中的第一个匿名内部类
2024/7/22
Set集合
List 是有序的,Set是无序的
取出的顺序和添加时的顺序是一样的,叫做有序
反之为无序,无序可能存在某次 取出时一样,但仅是特例(小概率事件)
HashSet
HashSet set=new HashSet(); 实例化出一个set集合
1.存放数据 set.add("23");
2.删除数据 set.remove("123");没有"123",未成功删去
3.查看已存储多少个数据 set.size();
set无法存放相同的数据(equals比较的),set可以存储null值
TreeSet
底层实现结构:红黑树 遍历方式是中序遍历
他的输出是有序的 按照从小到大依次输出
TreeSet不能存放null值 运行时会报空指针异常,因为null值不可比较,不知道放到那个节点上
TreeSet内部使用二叉树,内部节点是可以比较大小的
需要存入同种类型,因为不同种类型无法比较大小
同一个TreeSet对象存储的内容都应该是可比较的(同一类型)默认情况下不能存储不同类型
要让自己写的类变成可比较的(可以存入TreeSet),就要让该类实现Comparable接口,然后重写比较方法,或者使用比较器
LinkedHashSet是有序的
Map
map不属于collection
map存储键值对 键:名字 值:对象 键key是唯一的
HashMap
1.map.put("A1","张三");//存放数据
2.Object obj=map.get("A1");//获取A1对应的value值
3.Object obj_rem=map.remove("A1");//通过键A1删除键值对,并返回value对象
4.map.containsKey("A1");//是否包含该key 返回一个布尔值
map.containsValue("张三");
Set setkey=map.keySet();//获取所有的key 返回Set类型 Collection com=map.values();//获取所有的value 返回一个集合
5.可以存null值,但key只能存一个null值,因为key不能重复是唯一
map.put(null,null); map.put("A1",null);
TreeMap
TreeSet底层实现是TreeMap HashSet底层实现是HashMap
key应该是可以比较的,所以不能为null value可以为null
Hashtable
Hashtable ht=new Hashtable();
其中的t是小写,而且key和value都不能为空
ConcurrentHashMap
线程安全,效率高
HashMap的底层实现
数组加链表 :可以节省空间
HashMap的数组默认容量是16,每次扩容两倍
扩容阈值0.75:已使用空间达到当前容量的四分之三(0.75)便开始扩容
最小树化阈值8:数组元素中一个链达到8就对该链进行树化 ,一支树上的元素低于6个,就会退化为链
树化:加快检索效率
最小树化容量阈值64:数组的长度必须达到64,链上元素超过8时才会树化,如果数组长度没到64,优先扩容
线程安全的map有两个 Hashtable ConcurrentHashMap(性能优异,锁的颗粒度比较小)
泛型
泛型:广泛的数据类型
泛型是确保类型安全的一种途径
在类或方法中定义泛型
泛型在程序运行时不起作用,在编译时起作用,用于限定数据类型
public void test() {
ArrayList<String> list = new ArrayList<>();//
//泛型的检查在声明上进行(前面<>里的),在后面的<>里写的没用,不写就默认Object类型
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(111);// 在编译阶段,编译器会报错
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}
泛型类
public class Generic<T> {
// key 这个成员变量的数据类型为 T, T 的类型由外部传入
private T key;
// 泛型构造方法形参 key 的类型也为 T,T 的类型由外部传入
public Generic(T key) {
this.key = key;
}
// 泛型方法 getKey 的返回值类型为 T,T 的类型由外部指定
public T getKey(){
return key;
}
}
泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数
泛型类不只接受一个类型参数,它还可以接受多个类型参数
public class MultiType <E,T> {
E value1;
T value2;
public E getValue1(){
return value1;
}
public T getValue2(){
return value2;
}
}
如果 <> 中什么都不传入,则默认是 < Object >
7/22小测错题知识点整理
double d=1L; 1L是长整形long
构造方法不能继承,子类不会继承父类的构造方法
包是用来组织Java代码的,可以有效区分重名的java文件,使得他们的带路径名字不同
代码块的执行顺序:类中的静态代码块只会执行一次,其他代码块在每次调用构造方法时都会执行一次。总的执行顺序:父类的静态代码块-子类的静态代码块-父类的成员代码块-父类的构造方法-子类的成员代码块-子类的构造方法
String s = "a"+"c"+""+"d" 创建了一个对象 如果是在循环中依次添加的话,则会产生多个中间块
基本数据类型在自动转换时可能会出现精度的缺失,比如long向double转时可能出现精度缺失
二分查找法的时间复杂度为O(logn)
while(true) 是一个标准的死循环
2024/7/23
异常
异常:程序中一些程序处理不了的特殊情况
异常类Exception
继承关系:Throwable-->Exception-->RuntimeException
一般直接继承了Exception的为检查型异常 直接继承了RuntimeException的为运行时异常
Throwable-->Error(错误,事故,java程序无法处理)
异常的分类:
1.检查型异常(编译异常):在编译时就会抛出的异常(代码会报错),需要在代码中编写处理方式,检查型异常大多出现在和程序之外的资源进行访问的时候
2.运行时异常:在代码的运行阶段可能会出现的异常,可以不用明文处理
try...carch...fianlly
try:块尝试捕捉异常 catch:捕捉到异常后要怎样处理的代码 一般格式catch(错误类型对象){打印错误日志} finally:无论是否出现异常都会都会执行的代码块 ,一般用于关闭资源(流),一般关闭在finally里关闭流时也要用try catch try后面可以跟多个catch来捕捉多个异常 注意:先catch大异常,后面就不能catch更小的异常 有继承关系的化,小的异常要往前放,子类异常优先捕捉,父类异常后置处理 catch的合并处理方案(多个异常使用一个catch来捕捉): 1.使用|声明多种异常
catch(ArithmeticException|ClassCastException|FileNotFoundException e){ }
2.声明父类异常,捕捉子类异常(大异常小异常都是异常)
catch(Exception e)
eg: try { //try块尝试捕捉异常 其中是可能会抛出异常的代码 fileInputStream = new FileInputStream(file); //文件未找到 //fileInputStream.close(); 若是上面报错,下面终止就不会执行,一直消耗资源,所以使用finally }catch(FileNotFoundException e){ //捕捉到异常后要处理的代码 e.printStackTrace();//打印错误日志 }finally{ //无论是否出现异常都会执行的代码块 //一般用在关闭资源 if(fileInputStream!=null){ //为了防止fileInputStream文件不在同一个作用域访问不到, // 要放在整个try...catch...fianlly外面声明 try { fileInputStream.close(); } catch (IOException e) { throw new RuntimeException(e); } }; }
如果catch块抛出了异常没有fianlly的话就会中断程序,如果有finally,就会运行finally
如果finally正常运行,并且有返回,程序会正常运行
如果finally里有异常,就会报finally中的异常
try块不能单独编写
自定义异常
throws和throw
throws:声明方法可能会抛出那些异常,若抛出多种异常用“,”隔开
throw:当代码遇到问题时,具体抛出某个异常对象
public void Info() throws StudentNameIsNullException,NullPointerException, IOException { if(name==null){ throw new StudentNameIsNullException("Student name is null"); } //name==null 是一种特殊情况,不符合业务的需求 System.out.println("我的名字是"+name); }
运行时异常不需要用throws声明 也可以用throw抛出
public void InfoA() { if(name==null){ //运行时异常不需要强调去指出throws 如RuntimeException throw new NullPointerException("Student name is null"); } //name==null 是一种特殊情况,不符合业务的需求 System.out.println("空"); }
在方法重写时,子类重写的方法throw抛出的异常类只能更精确(范围更小)
class BigStudent extends Student{ //方法重写:子类对父类中继承过来的方法进行重新定义 返回值类型,方法名,参数列表不能变, // 访问权限只能更开放, // 重写的方法抛出的异常只能更精确(范围更小) public void info()throws NullPointerException,StudentNameIsNullException{ }
//检查型异常是Exception的直接子类 class StudentNameIsNullException extends Exception{ public StudentNameIsNullException(){ } public StudentNameIsNullException(String msg){ super(msg); } }
File常用方法
java中对文件操作时,要引入java.io包
1.在java中声明一个文件 ,传入字符串当做文件地址
File f=new File("D:\\easy.txt");
2.判断文件是否存在
boolean bool=f.isFile(); bool=f.exists();//文件是否存在
3.创建文件
f.createNewFile(); 返回一个boolean值,来表示是否创建成功
4.f.delete(); 删除文件,返回一个boolean值 当删除文件夹时,文件夹必须是空的
5.获取是否是文件或文件夹
bool=f.isFile();//是否是文件
bool=f.isDirectory();//是否是文件夹
6.创建文件夹
f.mkdir();//前面的路径必须存在 f.mkdirs();//前面的路径不存在会自己创建
IO输入输出流
IO 输入流/输出流
流动的是数据(二进制)
分类:1.根据流动的方向不同 输入流和输出流
2.根据流动的介质不同 字符流和字节流 字符流只能读取文本 如下
txt .xml .properities .html .yml
字节流可以读取任意得文件类型
只能由字节流转化为字符流
3.根据功能不同,分为节点流和功能流 节点流是直接连在文件上的 而功能流是对节点流的加工
创建对象的方式:1.new实例化 2.clone克隆 3.反序列化 4.反射
关闭流时要一个一个的关
finally{//关闭流时要一个一个关 if(oos!=null){ try{ oos.close(); }catch(IOException e){ e.printStackTrace(); } } if(fos!=null){ try{ oos.close(); }catch(IOException e){ e.printStackTrace(); } }
将内存对象转化为序列(流)的过程叫序列化,且该对象必须是序列化的
try{ fos=new FileOutputStream("D:\\easy.txt"); oos=new ObjectOutputStream(fos); oos.writeObject(staff); }
可序列化:实现了Serializable接口
class Staff implements Serializable{//可序列化 String name; @Override public String toString() { return "Staff{" + "name='" + name + '\'' + ", sex='" + sex + '\'' + ", salary=" + salary + '}'; } String sex; int salary; }
将对象序列读入程序,转化成对象的方式叫做 反序列化,反序列化会创建新的对象
try{ fis=new FileInputStream("D:\\easy.txt"); ois=new ObjectInputStream(fis); Object obj=ois.readObject(); System.out.println(obj); }
//字节输入流 InputStream is; //字节输出流 OutputStream os; //字符输入流 Reader r; //字符输出流 Writer w;
2024/7/24
序列化与反序列化
序列化
定义:将对象的状态信息转化为可以存储或传输的的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久的存储区。以后可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。 自我理解:讲对象写入到文件中存储起来
反序列化
定义:把字节序列还原为对象的过程称为反序列化。
Serializable实现序列化
在序列化时,要转换的对象必须是序列化的,即它的类实现了Serializable接口
public class Student implements Serializable{}
序列化方法的部分代码:
首先创建了FileOutputStream
对象fos
,用于向文件写入数据。然后,创建了ObjectOutputStream
对象oos
,它包装了fos
,并允许写入可序列化的对象。最后,通过调用oos.writeObject(stu)
,将stu
(假设是一个实现了Serializable
接口的Student
对象)序列化并写入文件。
public static void serStudent(Student stu) {//写出student FileOutputStream fos = null; ObjectOutputStream oos = null; File file = new File("D\\student.data"); if (!file.exists()) { try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } try { fos = new FileOutputStream(file); oos = new ObjectOutputStream(fos); oos.writeObject(stu); }
反序列化方法的部分代码:
代码创建了一个File
对象,它代表了文件系统中的一个文件或目录路径。
声明了两个变量fis
和ois
,并将它们初始化为null
。这些变量将用于创建文件输入流和对象输入流。在try
块中,代码首先使用file
对象创建了FileInputStream
对象fis
,然后通过fis
创建了ObjectInputStream
对象ois
。接着,它调用ois.readObject()
方法从文件中反序列化一个对象,并将其存储在Object
类型的变量obj
中。代码使用instanceof
操作符检查obj
是否是Student
类型的实例。如果是,它将obj
强制转换为Student
类型并返回。
File file = new File("D\\student.data"); FileInputStream fis = null; ObjectInputStream ois = null; try { fis = new FileInputStream(file); ois = new ObjectInputStream(fis); Object obj = ois.readObject(); if (obj instanceof Student) { return (Student) obj; }
反序列化后得到的对象时一个新的对象,与被用来序列化的不是同一个
序列化版本号serialVersionUID
serialVersionUID是一个类的序列化版本号
如果该量没有定义,JDK会自动给与一个版本号,当该类发生变化时,序列化版本号会发生变化,反序列化会失败
自定义该版本号,只要改版本号不发生变化,即使类中的属性和方法发生变化,该类的对象依旧可以反序列化
transient
transient是一个关键字,他修饰类中的成员变量,被修饰的变量不可以被序列化
线程
定义线程
线程类的父类是Thread,自定义的线程都要继承Thread并重写它的run方法
run方法里写线程要执行的任务,当创建线程后使用start开启线程,会自动调用run方法
线程是程序运行阶段的不同运行路线
线程的执行会有交叉,普通的调用方法则是按照顺序依次执行
Thread a=new ThreadA(); Thread b=new ThreadA(); //开启线程 使用start方法 同时执行,会有交叉 a.start(); b.start(); //普通的调用方法,从上往下按顺序执行,不会交叉 a.run(); b.run();
线程常用的方法
我们创建一个自定义的线程
Thread t=new ThreadA();
1.休眠方法sleep()
休眠方法是一个Thread的静态的类方法
我们可以直接Thread.sleep(5000);来表示线程此时休眠5s,里面的参数是毫秒,是long类型
休眠后会自动启动线程
2.获得线程的名字f.getName();
返回一个字符串类型,f进程的名字
3.获取当前线程对象Thread.currentThread()
返回所在的线程对象,通常与f.getName()一起使用
Thread.currentThread().getName()
4.设置优先级setPriority()
Thread a=new ThreadB();
Thread b=new ThreadB();
//设置优先级
a.setPriority(4);
b.setPriority(6);
优先级越高,获得时间片的可能越大,优先级从1到10,默认为5,设置其他值会报错
5.礼让yeild()
静态方法 使用
Thread.yield();调用
//yeild 礼让 让出资源,尝试让CPU 重新分配资源,避免同一个资源一直占用CPU,达到CPU资源合理分配的效果 //使用sleep(0)也可以达到类似效果 ,一瞬间停止,CPU回收资源,然后开始重新分配资源
6.插队join()
成员方法
//在A线程中执行了B.join(), B线程先运行完毕后,A线程才能运行
关闭线程
三种方法
//1.执行stop方法 不推荐使用 //2.设置中断状态,调用interrupt()设置中断状态,这个线程不会中断,我们需在线程内部判断中断状态是否被设置,然后执行中断操作 isInterrupted判断中断状态 //3.自定义一个状态属性,在线程外部设置此属性,影响线程内部的运行
1.
Thread a=new ThreadE(); a.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } a.stop();
2.
Thread thread=new ThreadF(); thread.start(); try { Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } thread.interrupt(); }
线程的生命周期
线程安全
多个线程操作一个对象,不会出现结果错乱的情况(数据缺失)
StringBuilder是线程不安全的
实现Runnable接口来创建线程:
这种方式更加灵活,因为它允许你的类继承其他类
//实现Runnable接口 不是线程类,也需要实现run方法 class RunA implements Runnable{ StringBuffer strB; // StringBuilder strB; public RunA(StringBuffer strB){ this.strB=strB; } @Override public void run() { for(int i=0;i<1000;i++){ strB.append("0"); } } }
然后在主类中创建RunA的对象,再将它传入线程的构造方法中
RunA r=new RunA(strB); Thread a=new Thread(r); a.start();
synchronized
线程同步:线程同步是指两个或多个线程协同步调,按预期的顺序执行代码,若两个或多个线程同时访问同一个共享资源时,需要让多个线程之间按照顺序访问
要做到线程安全,我们可以使用 synchoronized对方法或者代码块加锁,达到线程同步的效果
使用synchronized关键字修饰的同步方法或同步代码块,同一时间内只能有一个线程执行此代码
当你使用 synchronized (SyncThreadB.class)
时,你实际上是在锁定 SyncThreadB
类的 Class
对象。这意味着,在同一时刻,只有一个线程可以执行这个 synchronized
代码块内的代码,
public static void testA(){ System.out.println("**********进入方法"+Thread.currentThread().getName()); synchronized (SyncThreadB.class){ System.out.println("**********进入同步代码块"+Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("**********结束同步代码块"+Thread.currentThread().getName()); } }
//锁对象 使用 synchronized 需要指定锁对象 // synchronized 修饰方法 // 成员方法 this // 静态方法 类的类对象 obj.getClass() Easy.class //锁的分类 //根据有无锁对象 悲观锁,乐观锁 //悲观锁有锁对象 乐观锁没有锁对象 //synchronized是悲观锁 //乐观锁的实现方式 CAS 和版本号控制 //根据公平度不同 分为公平锁和非公平锁 公平锁就是先来后到 java大多是非公平锁 //可重入锁:在同步代码块中遇到相同的锁对象,不需要再获取锁对象的权限,直接进入执行 ///java中全部都是可重入锁 //根据线程的状态不同 偏向锁,轻量级锁(自旋锁),重量级锁 是相对的 // 绿灯 多等几秒 熄火等待
乐观锁的实现方式:
乐观锁(Optimistic Locking)是一种用于管理并发数据访问的技术,它假设在大多数情况下,数据冲突(即多个事务同时尝试修改同一数据)是不常见的。因此,乐观锁在数据更新时才进行冲突检查,而不是在数据读取时就锁定资源。如果检测到冲突(即数据自读取以来已被其他事务修改),则当前事务将被回滚,并可以重新尝试。
乐观锁的实现方式主要有以下几种:
1. 版本号(Version Number)
版本号是最常用的乐观锁实现方式。在数据库表中添加一个“version”字段,每次更新数据时,版本号加1。读取数据时,将版本号一起读出,数据更新时,检查当前数据库中的版本号与第一次读出来的版本号是否一致,如果一致则更新,并将版本号加1;如果不一致,则说明数据已被其他事务修改过,则回滚当前事务。
2. 时间戳(Timestamp)
时间戳和版本号类似,也是在数据库表中添加一个字段,不过该字段存储的是数据最后更新的时间戳。更新数据时,检查当前数据库中的时间戳与读取时的时间戳是否一致,如果一致则更新数据并更新时间戳,如果不一致则回滚事务。时间戳的好处是它可以自动获取,而不需要每次更新时手动增加。
CAS
在Java中,CAS(Compare and Swap,比较并交换)是一种并发编程中常用的原子操作,也被称为无锁算法。它主要用于解决多线程环境下的数据竞争问题,提供了一种非阻塞的同步方式。以下是对CAS的详细解释:
一、定义与原理
- 定义:CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。
- 原理:CAS操作在底层是由处理器支持的,是一种原语。原语是操作系统或计算机网络用语范畴,是由若干条指令组成的,用于完成一定功能的一个过程,具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断。
二、特点与优势
- 原子性:CAS操作是原子的,它在执行时会将内存位置的值与预期值进行比较,并在比较成功时将新值写入内存位置,整个过程是原子的,不会被其他线程打断。
- 非阻塞:CAS操作不需要使用传统的锁机制(如synchronized关键字)来保证线程安全,因此它能够有效地减少线程等待的时间,提高并发性能。
- 高效性:由于CAS操作避免了锁的使用,减少了线程上下文切换的开销,因此在高并发场景下性能较好。
三、应用场景
- CAS常被用于实现无锁的数据结构、原子操作等,例如Java中的
AtomicInteger
、AtomicLong
等类。这些类内部使用了CAS操作,提供了一些基本数据类型的原子性操作。 - 在多线程编程中,CAS可以用来保证共享变量的安全性,而不需要显式地使用锁。
四、实现方式
- 在Java中,CAS通常通过
sun.misc.Unsafe
类来实现,该类提供了一系列的底层操作,包括CAS操作。但需要注意的是,sun.misc.Unsafe
是JDK内部使用的API,因此在正式的生产代码中并不推荐直接使用它。 - 另一种更常见的方式是通过
java.util.concurrent.atomic
包下的原子类来间接使用CAS操作。这些原子类提供了丰富的原子操作方法,如compareAndSet()
、getAndIncrement()
等。
五、问题与限制
- ABA问题:当一个值被改变为其他值,然后再改回原值时,CAS无法察觉到这个变化。这可能导致一些逻辑上的错误。为了解决这个问题,JDK 1.5之后新增了
AtomicStampedReference
类,它通过在值上添加版本号来避免ABA问题。 - 循环时间长开销大:如果有很多个线程并发,CAS自旋可能会长时间不成功,这会增大CPU的执行开销。因此,在设计并发算法时需要考虑这一点。
- 只能对一个变量进行原子操作:CAS操作通常只能保证单个变量的原子性,如果需要保证多个变量的原子性,则需要使用其他机制(如锁)。不过,JDK 1.5之后新增了
AtomicReference
类,它可以将多个变量放入一个对象中,然后通过CAS操作来保证这个对象的原子性。
综上所述,CAS是Java并发编程中一种重要的同步机制,它通过比较并交换内存位置的值来实现原子操作,是实现高效并发编程的重要工具之一。然而,在使用时也需要注意其问题和限制,以避免出现意外的错误。
版本号控制
在Java中,版本号控制(Version Control)通常不是指Java语言本身提供的某种特定机制,而是指开发者在软件开发过程中用于管理代码变更的一套系统或工具。版本号控制对于团队协作、代码追踪、历史回顾和回滚等方面至关重要。
虽然Java没有内置的版本号控制功能,但开发者可以使用多种外部工具来实现版本控制,其中最流行的是Git。Git是一个开源的分布式版本控制系统,它可以有效地管理任何大小的项目,并且支持几乎所有类型的开发流程。
Git中的版本号
在Git中,版本号通常指的是提交(Commit)的哈希值,这是一个由40位十六进制字符组成的唯一标识符,用于唯一标识每次提交。然而,为了简化,人们通常使用哈希值的前几位(如7位)来指代特定的提交。
此外,Git还允许开发者使用标签(Tag)来给特定的提交打上版本号标签,这些版本号通常遵循一定的命名规范,如v1.0
、v1.1
等。这样,开发者就可以通过版本号来引用特定的提交,从而更容易地进行版本管理和发布。
Java项目中的版本号
在Java项目中,除了使用Git等版本控制系统来管理代码变更外,项目本身也会有一个版本号,这个版本号通常用于标识项目的不同发布版本。Java项目的版本号通常遵循一定的命名规范,如语义化版本号(Semantic Versioning,简称SemVer)。
语义化版本号由三部分组成:主版本号、次版本号和修订号,它们之间用点(.)分隔,格式如下:主版本号.次版本号.修订号
- 主版本号:当你做了不兼容的API修改时,需要增加主版本号。
- 次版本号:当你做了向下兼容的功能性新增时,需要增加次版本号。
- 修订号:当你做了向下兼容的问题修正时,需要增加修订号。
例如,版本号1.0.0
表示项目的第一个稳定版本;1.1.0
表示在第一个稳定版本的基础上增加了新功能,但仍然保持向下兼容;1.0.1
则表示在第一个稳定版本的基础上进行了问题修正,但没有增加新功能。
在Java项目中,版本号通常会在项目的构建配置文件(如Maven的pom.xml
或Gradle的build.gradle
)中指定,以便在构建和发布项目时能够正确地标记版本。此外,一些项目还会在项目的源代码或文档中包含版本号信息,以便用户能够清楚地知道他们正在使用哪个版本的软件。
BIO
JAVA中的BIO,全称为Blocking I/O(阻塞式I/O),是Java中最传统的I/O模型,也是最早被支持的I/O模型。BIO模型的主要特点在于其同步阻塞的IO操作方式。
BIO模型的特点
- 同步阻塞:
- 在BIO模型中,当一个线程从输入流读取数据或向输出流写入数据时,线程会被阻塞,直到有数据可读或数据完全写入。这意味着线程在等待IO操作时不能执行其他任务,这可能导致资源(如CPU和内存)的浪费。
- 线程模型:
- BIO模型采用一个连接一个线程的模型,即客户端有连接请求时,服务器端就会启动一个新的线程来处理这个连接。这种方式在连接数较少时是可行的,但当连接数增加时,线程数量也会迅速增加,从而导致资源消耗过大,甚至可能引发系统崩溃。
- 资源消耗:
- 由于每个连接都需要一个独立的线程来处理,BIO模型在处理大量并发连接时,会消耗大量的系统资源(如CPU和内存),这限制了其在大规模并发场景下的应用。
BIO模型的应用场景
- BIO模型适用于连接数较少且吞吐量要求不高的场景,如传统的Socket编程或简单的Web应用服务器。在这些场景中,BIO模型由于其简单易懂、易于实现的优点,仍然具有一定的应用价值。
BIO模型的改进
为了改善BIO模型在处理大量并发连接时的性能问题,可以使用线程池来管理线程。线程池能够限制同时运行的线程数量,避免了线程数量的无限制增长,从而在一定程度上缓解了资源消耗过大的问题。然而,即使使用线程池,BIO模型在处理高并发连接时仍然存在一定的局限性。
NIO
JAVA中的NIO(New Input/Output),即新的输入/输出,是Java 1.4及以上版本中引入的一套全新的IO处理机制。相比传统的BIO(Blocking I/O,阻塞式I/O)模型,NIO提供了更高效、更灵活的I/O操作方式,特别是在网络编程和高并发场景下表现尤为出色。
NIO的核心组件
NIO主要包括以下几个核心组件:
- Channel(通道):
- Channel是数据传输的载体,可以与文件或网络套接字建立连接。它是双向的,支持读写操作,与BIO中的Stream(流)不同,Stream是单向的。
- Java NIO中的主要Channel类型包括:FileChannel(用于文件读写操作)、DatagramChannel(用于UDP协议的网络通信)、SocketChannel(用于TCP协议的网络通信)、ServerSocketChannel(用于监听TCP连接请求)。
- Buffer(缓冲区):
- 缓冲区是一块连续的内存区域,用于存储数据。它是NIO操作数据的中转站,数据通过Buffer进行传输,从Channel读取到Buffer,从Buffer写入到Channel。
- Java NIO为不同的数据类型提供了相应的缓冲区类型,如ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer等。
- 缓冲区的主要属性包括:容量(capacity)、限制(limit)、位置(position)和标记(mark)。这些属性共同管理缓冲区的数据读写操作。
- Selector(选择器):
- Selector用于检测一个或多个Channel的状态,并且可以根据Channel状态进行非阻塞选择操作。它允许单线程管理多个Channel,以此实现高并发IO操作。
- 当有一个或多个Channel的事件就绪时(如连接就绪、读取数据就绪、写入数据就绪等),Selector会自动返回这些Channel的选择键(SelectionKey),然后可以通过选择键获取到对应的Channel,进行相应的操作。
NIO的特点
- 非阻塞IO:
- NIO支持非阻塞IO操作,即线程可以在没有数据可读或可写时继续执行其他任务,而不是像BIO那样在等待IO操作时被阻塞。
- 缓冲区:
- 使用缓冲区来管理数据,使得读写操作更加快速和灵活。数据通过缓冲区进行传输,减少了直接对Channel的操作,提高了效率。
- 选择器:
- Selector允许单线程管理多个Channel,减少了线程的数量和上下文切换的开销,提高了系统的并发能力。
NIO的优缺点
优点:
- 提高了I/O操作的效率和灵活性。
- 减少了线程的数量和上下文切换的开销,提高了系统的并发能力。
- 适用于高并发、大规模数据处理的场景。
缺点:
- 相对于BIO模型,NIO模型的编程复杂度较高,需要开发者对Channel、Buffer、Selector等组件有较深的理解。
- 在某些场景下,如小文件读写或低并发场景下,BIO模型的性能可能更优。
应用场景
NIO特别适用于需要高并发、大规模数据处理的场景,如网络服务器、文件服务器、数据库连接池等。在网络编程中,NIO可以极大地提高网络通信的效率和可靠性。
综上所述,JAVA中的NIO是一种高效、灵活的I/O处理机制,通过引入Channel、Buffer、Selector等核心组件,实现了非阻塞IO操作和高并发处理。它是Java网络编程和高性能I/O操作的重要工具之一。
AIO
Java中的AIO(Asynchronous I/O,异步非阻塞I/O)是Java NIO的一个扩展,它提供了更高级别的异步I/O操作。以下是对Java AIO的详细介绍:
一、基本介绍
- 引入版本:Java AIO是在JDK 7中引入的,作为Java NIO(New Input/Output)的扩展,旨在提供更高效的异步I/O处理能力。
- 主要特点:AIO允许应用程序执行非阻塞I/O操作,而无需使用Selector和手动轮询事件的方式。当I/O操作完成时,操作系统会通知应用程序,而不需要应用程序主动查询或等待操作完成。
- 模式:AIO采用了Proactor模式,与Reactor模式(Java NIO所采用的模式)不同,Proactor模式是由操作系统主动通知应用程序I/O操作的结果,而不是由应用程序轮询查询。
二、核心组件
- AsynchronousServerSocketChannel:异步服务器套接字通道,用于服务器端的异步非阻塞I/O操作。它允许服务器通过注册感兴趣的事件(如连接请求),并在事件发生时异步地执行处理。
- AsynchronousSocketChannel:异步套接字通道,用于客户端的异步非阻塞I/O操作。它允许客户端通过注册感兴趣的事件(如读、写操作),并在事件发生时异步地执行处理。
- CompletionHandler<V,A>:事件处理类(回调函数),定义了异步操作成功和失败时的回调方法。当异步操作完成时,系统会调用这些方法,并将操作结果或异常信息传递给它们。
三、工作原理
- 在AIO中,应用程序发起I/O操作(如读、写、连接等)后,可以继续执行其他任务,而无需等待I/O操作完成。
- 当I/O操作完成时,操作系统会主动通知应用程序,并通过注册的CompletionHandler回调方法将操作结果或异常信息传递给应用程序。
- 应用程序在回调方法中处理I/O操作的结果,如读取数据、写入数据或处理连接请求等。
四、优势与应用场景
- 优势:
- AIO可以显著地降低线程数量,提高应用程序的性能和吞吐量。
- 它适用于处理大量连接或高并发的场景,能够更有效地利用系统资源。
- 通过异步操作,应用程序可以更灵活地控制I/O流程,提高程序的响应性和可扩展性。
- 应用场景:
- 高并发网络服务器:如Web服务器、数据库连接池等。
- 实时数据处理系统:如实时日志分析、实时消息处理等。
- 需要高性能I/O操作的场景:如文件传输、大数据处理等。
五、注意事项
- 尽管Java AIO提供了一种高效的异步I/O模型,但它目前还没有像Java NIO那样得到广泛应用。这可能是因为AIO的编程复杂度相对较高,且在某些场景下(如小文件读写或低并发场景)的性能优势并不明显。
- 在使用Java AIO时,需要注意正确处理异步操作的结果和异常,以确保程序的稳定性和可靠性。
总的来说,Java AIO是Java NIO的一个重要扩展,它提供了更高级别的异步I/O操作能力。通过合理利用Java AIO,可以构建出更高效、更可靠的高并发网络应用和数据处理系统。
volatile
关键字在Java中是一个非常特殊的类型修饰符,它主要用于多线程编程中,以确保变量对所有线程的可见性和有序性。下面详细解释volatile
的作用以及为什么需要它。
volatile
volatile
的作用
-
保证可见性:
当多个线程访问同一个变量时,如果这个变量被声明为volatile
,那么当一个线程修改了这个变量的值,新值对其他线程来说是立即可见的。这是因为volatile
修饰的变量在每次被线程访问时,都会直接从主内存中读取它的值,而不是从线程的工作内存中读取。同样,对这个变量的修改也会直接写回到主内存中,其他线程再次读取时就能获取到最新值。 -
禁止指令重排序:
在Java中,为了提高程序运行效率,编译器和处理器可能会对指令进行重排序。然而,在多线程环境下,指令重排序可能会导致程序出现不可预测的行为。volatile
关键字可以禁止指令重排序,从而确保程序的执行顺序与代码顺序一致,这对于保持程序的正确性非常重要。
为什么需要 volatile
在多线程编程中,线程之间共享数据是非常常见的。然而,由于每个线程都有自己的工作内存(也称为线程本地存储),它们对共享变量的访问可能不会立即对其他线程可见。这可能导致一个线程修改了某个变量的值,但这个新值对其他线程来说是不可见的,从而引发数据不一致的问题。
此外,即使变量在多个线程之间是共享的,并且每个线程都读取和修改这个变量,但如果没有适当的同步机制(如synchronized
或volatile
),那么编译器和处理器可能会对这些操作进行重排序,以优化性能。这种重排序可能会导致程序在单线程环境中运行正常,但在多线程环境中却表现出不一致的行为。
因此,为了确保多线程环境下的数据一致性和程序的正确性,我们需要使用volatile
关键字来修饰那些可能会被多个线程同时访问的变量。通过保证这些变量的可见性和禁止指令重排序,我们可以避免许多常见的多线程问题,如脏读、不可见性和指令重排序导致的错误。
然而,需要注意的是,volatile
并不能保证原子性。也就是说,如果变量是一个复合类型(如数组或对象),或者需要执行多个操作(如i++
),那么仅仅使用volatile
是不够的,还需要使用其他同步机制(如synchronized
或java.util.concurrent
包中的类)来确保操作的原子性。
2024/7/25
创建线程的方式
1.类继承Thread 重写run方法 创建该类的对象
2.使用lambda表达式(匿名函数)
Runnable run=EasyThreadA::method; // 用后面类中的method方法来替换掉接口里面未实现的方法
Thread t=new Thread(run);
Thread f=new Thread(run);
Runnable run= new EasyThreadB()::method;//动态方法,要用new一个对象来调用
lambda表达式 如果函数式接口的实现是一个静态方法 ,就直接如上:类名::静态方法
两者的区别:若是使用线程类创建多个线程,则线程之间是不相关的,若是实现Runnable接口创建多个线程,则线程共同操作传入的对象,如上的run对于线程 t 和 f 是共享的
锁对象 Lock
Lock lock=new ReentrantLock(); 创建一个Reentrantlock实例的锁对象
lock.lock();//加锁
lock.tryLock()//与加锁类似,但还会返回一个布尔值表示加锁是否成功
lock.unlock();//解锁
ReentrantReadWriteLock
ReentrantReadWriteLock
是 Java 并发包 java.util.concurrent.locks
中的一个类,它实现了 ReadWriteLock
接口。ReentrantReadWriteLock
是一种允许高并发级别的锁,它通过将锁的访问权限细分为读锁和写锁,来优化读操作的性能。在多个线程同时读取共享资源时,读锁可以被多个线程同时持有,从而提高程序的并发性;而在写入共享资源时,写锁是独占的,以确保数据的一致性。
public static ReentrantReadWriteLock rrwl=new ReentrantReadWriteLock();
Lock lock= rrwl.readLock(); lock.lock();lock.unlock();
Lock lock= rrwl.writeLock();lock.lock();lock.unlock();
wait()和notify()
在Java中,wait()
和 notify()
/notifyAll()
方法是与对象监视器(monitor)一起工作的,它们用于在多个线程之间进行通信。这些方法属于 Object
类,因此每个Java对象都可以作为锁来使用,并且都拥有这些方法。然而,需要注意的是,这些方法只能在同步方法或同步代码块中被调用,因为它们依赖于对象监视器的锁状态。
wait()
wait()
方法会使当前线程进入等待(阻塞)状态,直到其他线程调用此对象的notify()
方法或notifyAll()
方法。- 调用
wait()
方法时,当前线程必须持有该对象的锁。在调用wait()
方法后,当前线程会释放这个锁,并进入等待状态。 - 当其他线程调用此对象的
notify()
方法时,会随机唤醒一个在该对象上等待的线程(如果有的话)。如果调用的是notifyAll()
方法,则会唤醒所有在该对象上等待的线程。 - 被唤醒的线程会重新尝试获取对象的锁,然后继续执行其后续操作。
notify()
notify()
方法用于唤醒正在等待该对象监视器的单个线程。- 调用
notify()
方法之前,当前线程必须持有该对象的锁。 notify()
方法不会立即释放锁,而是在执行完同步代码块或同步方法后,当前线程退出同步块时释放锁。- 需要注意的是,
notify()
方法并不能保证唤醒的是等待时间最长的线程,而是随机唤醒一个等待的线程
notifyAll()
notifyAll()
方法用于唤醒正在等待该对象监视器的所有线程。- 与
notify()
方法一样,调用notifyAll()
方法之前,当前线程必须持有该对象的锁。 - 调用
notifyAll()
方法后,当前线程会在退出同步块时释放锁,此时所有等待的线程都将被唤醒并尝试重新获取锁。
- //wait和sleep的区别 //wait 是Object中定义的方法,可以有锁对象调用,让执行到该代码的线程进入到等待状态 //sleep是Thread类中定义的静态方法 ,可以让执行到该行的线程进入等待状态 //区别: 1.sleep需要传入一个毫秒数 ,到达时间后会自动唤醒 // wait不能自动唤醒,必须调用notify/notifyAll方法唤醒 // 2.sleep方法保持锁状态进入等待状态 wait方法会解除锁状态,其他线程可以进入运行
线程池
完成线程的创建和管理、销毁线程的工作
预先创建并管理一定数量的线程,并让这些线程处于等待状态。当程序需要执行新的任务时,它会从线程池中取出一个空闲的线程来执行该任务,而不是创建一个新的线程。这样做的好处是可以减少线程的创建和销毁的开销,提高系统的响应速度和吞吐量,同时可以避免因大量线程的创建和销毁导致的系统资源消耗和性能下降。
使用ThreadPoolExecutor创建线程池
BlockingQueue qe=new ArrayBlockingQueue(12);
ThreadPoolExecutor tpe=new ThreadPoolExecutor(5,10,10,TimeUnit.SECONDS,qe,Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
线程池对象需要用shutdown()来关闭
//线程池对象需要关闭 tpe.shutdown();
1.线程池的7个参数
corePoolSize核心线程数 :线程中的核心线程数 ,默认情况下核心线程会一直存活在线程池中,即使他们处于空闲状态。如果allowCoreThreadTimeOut设置为true,则核心线程在空闲时间超过heepAliveTime后也会被终止
maximumPoolSize最大线程数: 线程池允许的最大线程数。当线程池中的线程数达到这个值,如果再有新任务提交,则会根据拒绝策略来处理这些任务
keepAliveTime线程空闲时间: 当线程数大于核心线程数时,这是多余线程在终止前等待新任务的最长时间,配合TimeUnit使用,以指定时间单位
TimeUnit(时间单位): keepAliveTime的时间单位,常有的有SECONDS等
workQueue(任务队列)
- 用于保存等待执行的任务的阻塞队列。
- 当所有核心线程都忙时,新任务会被添加到这个队列中等待执行。
- 常用的队列类型有 LinkedBlockingQueue(无界队列)、ArrayBlockingQueue(有界队列)、SynchronousQueue(不存储元素的阻塞队列)等。
threadFactory(线程工厂)
- 用于创建新线程的工厂。
- 可以通过实现 ThreadFactory 接口来自定义线程的创建方式,如设置线程的优先级、守护线程状态等。
回绝策略
- 当线程池无法处理新任务时(即线程池已满,无法再添加新任务到任务队列中),会执行拒绝策略。
-
// AbortPplicy(默认) 放弃该任务并会抛出一个异常RejectedExecutionException // CallerRunsPolicy 调用者执行,让传递任务的线程执行此任务 // DiscardOldestPolicy 放弃队列中时间最长的任务,不会抛出异常 // DiscardPolicy 直接放弃队列放不下的任务,不会抛异常
线程池的工作原理 // 任务放置在工作队列当中 //1>池中是否有空闲的线程,如果有让该线程执行任务 //2>如果池中没有空闲的线程,判断线程数量是否达到核心线程数 //3>如果没有达到,创建新的线程执行任务,直到填满核心线程数,如果已经达到,优先在队列中存储 // 直到队列填满, //4>工作队列填满之后再添加新的任务,是否达到最大线程数,如果没有,创建新的线程执行任务 //直到填满最大线程数 //5>已经填满最大线程数,队列也已经填满,没有空闲的线程,就执行回绝策略 //线程池中的线程达到(超过)核心线程数,超出的数量会根据存货时间,进行销毁,直到数量达到核心线程数 //如果线程的数量小于核心线程数,不会消亡
//4java中内置的线程池对象 : //可以根据工作任务创建线程,如果没有空闲的线程就创建新的线程 线程存活时间60s // Executors.newCachedThreadPool(); //设定最大线程数量的线程池 // Executors.newFixedThreadPool(10); //提供定时运行的处理方案 // Executors.newScheduledThreadPool(10); //创建一个具有单个线程的线程池,保障任务队列完全按照顺序执行 // Executors.newSingleThreadExecutor();
Runnable Callable
Runnable run=EasyExecuters::method; tpe.execute(run);tpe.submit(run);
Callable<String> call=EasyExecuters::methodCall; tpe.submit(call);
Future<String> f=tpe.submit(call); System.out.println(f.get());//会等待线程执行完毕
Runnable类的对象可以使用tpe.execute(run)和tpe.submit(run)方法,tpe是一个线程池对象
方法将安排 run
对象的 run()
方法在将来的某个时间点由池中的一个线程执行。
Callable
接口的call()
方法有返回值-
Future<String>:当你通过
submit
方法提交一个Callable
任务时,你会立即得到一个Future<String>
对象。这个对象代表了异步计算的结果。你可以通过调用get()
方法来获取结果,但get()
方法会阻塞调用线程,直到计算完成。
枚举类
/枚举类 默认继承Enum 但不能写extends Enum 需要用enum关键字在类名前声明 //首行 必须枚举所有的实例
枚举类的所有实例都在首行标出
public enum EasyColor { RED,YELLOW,GREEN,BLUR,PINK; public void printColor(){ System.out.println(this.name()); System.out.println(this.ordinal()); } } class Test{ public static void main(String[] args) { EasyColor.GREEN.printColor(); } }
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁的发生通常需要满足以下四个必要条件:
- 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,...,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
2024/7/26
反射
有对象之前必先有类 static来修饰类的属性和方法
在java中存储了类的内容,这个内容也应该是一个对象
java中用到的每一个类都有一个内存,每一块内存都是一个对象
这些对象记录了这些类中声明了那些属性和方法和构造方法
java将这些类声明为一个Class类
获取类的类对象
Class类的对象不能使用new来创建
1.使用 类名.class 来创建对象
Class clazz= EasyClassA.class; 其中EasyClassA使我们自己创建类
2.使用 类实例的getClass()方法
clazz=new EasyClassA().getClass();
3.使用特殊类Class的forName()方法
clazz=Class.forName("com.easy725.EasyColor");//里面写类的全名
什么是反射
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.
在程序运行期间,可以动态获取类中定义的属性和方法以及构造方法的机制(思想)的实现
反射的核心是Class类,程序中使用的类,每一个都有唯一对应的Class对象
反射的API:Field Method Constructor
Field
Field类可以获取类的属性信息
Class c=Easy.class; Class类c指向Easy类
Field fName=c.getField("name"); fName变量指向的就是Easy中的name属性
Object objectName=fName.get(easy); 获取该属性的值存入objectName
fName.set(easy,"李四");//前面是对象,后面是内容
//getFiled/getFields只能获取类中的 public 声明的属性
Field fCode=c.getDeclaredField("code");getDeclaredField可以获取自定义的属性
反射访问私有属性 必须先获取访问权限 fAddress.setAccessible(true);//将这个权限设置为true就可以访问
因此可知 反射会破坏类的封装性
method
//获取类对象 Class c=Easy.class;
通过这个Class
对象来创建Easy
类的一个新实例,可以使用newInstance()
方法
Easy easy=(Easy)c.newInstance();
Method ma=c.getMethod("methodA");
获取指定名称的公共方法
//调用方法 //method.invoke(对象) 反射
ma.invoke(easy); 此时可以通过Method类的对象ma调用参数为Easy类的easy对象的invoke方法来调用methodA方法
Constructor
反射获取构造方法
Class<Easy> c=Easy.class; c.newInstance();
Constructor<Easy> con=c.getConstructor();//调用无参构造方法 con=c.getConstructor(String.class);//调用含参构造方法
con.newInstance("张三");此时可以以此传入参数“张三”来调用构造方法
//修饰符 使用Modifer的方法判断方法/属性/构造方法的修饰符 Field f=c.getDeclaredField("test"); int fmod=f.getModifiers(); boolean bool= Modifier.isStatic(fmod); System.out.println(bool);
内省
内省通过反射来实现,但是内省不会破坏封装性
内省获取属性的读方法和写方法
getter/setter来获取和设置属性的内容
BeanInfo
此时我们有一个Class类的c指向Easy类
//获取BeanInfo BeanInfo bi=Introspector.getBeanInfo(c);
现在你可以使用bi来获取Easy的更多信息
调用BeanInfo
对象的getPropertyDescriptors()
方法来获取一个PropertyDescriptor
数组。这个数组包含了关于JavaBean所有属性的信息,每个PropertyDescriptor
对象代表了一个属性。
PropertyDescriptor[] pds=bi.getPropertyDescriptors();
String pName=pds[0].getName();//获取属性的名字 System.out.println("属性名:"+pName); Method read =pds[0].getReadMethod();//对应属性的getter方法 Method write=pds[0].getWriteMethod();//对应属性的setter方法
Easy easy=c.newInstance(); write.invoke(easy,"张三"); 创建实例easy并且使用从getWriteMethod获取来的setter方法为该实例赋值中的属性赋值“张三”