不是关注解决问题的具体步骤,而是建造解决问题所使用的对象,通过对象去描述在具体解决问题时的作用;你只需要关注解决问题所用到的对象,而不用关心具体过程;https://zhuanlan.zhihu.com/p/75409853
面向对象的四大特性:封装、继承、多态、抽象https://blog.csdn.net/zzd864582451/article/details/85335748
如何提升java高性能?可以使用内联(方法内联就是把调用方函数代码复制到调用方函数中,减少函数调用的开销);当一个方法内联后,虽然可以提升性能,但是如果被继承,还需要类型检查等许多问题,导致并没有太出色;所以当对使用多次的方法进行内联时,可以使用final、static、private修饰,避免继承导致类型检查https://www.cnblogs.com/xyz-star/p/10152564.html
关键字class:表明java程序将其中的全部内容包含在类中;
类名首字母大写,不能以数字开头,可以包含数字,字母,_,$;遵守驼峰准则;
运行已经编译的程序时,jvm总是从指定的类中的main方法的代码开始执行
java是一种强类型语言mean‘s每一个变量都要声明一种类型
基本类型
java没有任何无符号形式的类型;https://blog.csdn.net/dangerous_fire/article/details/62230972
byte、int、short、float、long、double、char、Boolean
float并不能满足需求,一般不在数字后加f(3.14f)默认为double类型
因为浮点数不适用于无法接受舍入误差的金融计算。(2.0-1.1不会等于0.9而是等于0.8999999)
如果不能有误差因该使用bigdecimal类
char:现在unicode可以用一个char来表示,有的需要两个;强烈不建议在java中使用char类型
变量命名:字母、数字、下划线、$(不建议在变量中使用)驼峰
变量初始化在声明之后要显示进行,使用未初始化的变量的值,java编译器认为是错的;
int a=1;or int a;a=1;
变量的声明尽可能的靠近第一次使用的地方;java10对于局部变量,如果从初始值可以推断出他的类型,就不用在声明类型,直接使用var(var a=1;)
常量中,使用fianl来修饰,且只能赋值一次;
如果希望一个常量可以在多个类中使用,可以定义一个类常量(public static final int A=1),类常量定义于main方法的外部
枚举类型
算数运算符:加、减、乘、除、取余%
将一个类标记为strictfp,将会使这个类的所有方法严格的使用浮点计算
java中的幂运算(double y=Math.pow(x,a)x的a次方)
在一个类中使用Math中的函数,可以在导入Math包里加上static(import static java.lang.Math.*)
这样就可以直接使用PI而不是Math.PI
关于floorMod的使用https://blog.csdn.net/scarecrow_fly/article/details/105834533
在数值之间的类型转换中,小向大转换不会精度丢失,但大向小转换会丢失精度;
自增自减(建议不使用)
a++ 是先使用a的数值在进行自加 ++a是先自加后使用a的值(a=2 a++ 先2后3 ++a 先3后3)
三元表达式
x < y ? x : y 如果x小于y则为x,否则为y
短路与或(&&,||)如果第一个操作数可以明确表达式的值,第二个操作数不必计算
&&如果第一个表达式为false则结果不可能为true,也就不用计算第二个表达式了
||如果第一个表达式为true,直接自动为true
位运算符
计算机中的数在内存中都是以二进制形式进行存储的,用位运算就是直接对整数在内存中的二进制位进行操作,因此其执行效率非常高,在程序中尽量使用位运算进行操作,这会大大提高程序的性能。
&都为真才为真 |都为假才为假 ^相同为真不同为假
>>右移运算 <<左移运算
每移动一位是2的x次方+-
左移是在原来数值上又加移动了几次方(4<<2是4往左移了两位,4是2^2,移动两位成2^4)右移是减
通过位于运算进行数字交换
a ^= b;//a^b=c b ^= a;//b^c=a-->a^b^a=a a ^= b;//a^b^a=b
关于位的操作https://www.zhihu.com/question/38206659
运算符级别
从左向右运算a&&b||c==(a&&b)||c
从右向左运算a+=b+=c-->a+=(b+=c)
从右向左运算
!~ ++ -- + - 强制类型转换;?:;= += -= *= /= %= &= |= ^= <<= >>= >>>=;
字符串
字符串就是Unicode字符序列在概念上
String all=String.join("/","f","o");//用“/”将后续的内容以斜线分割开
在string中的toUpperCase()方法中,字符串保持不变,会返回一个将字符大写的新字符串;
java字符串中不能修改字符串,但是可以使用substring方法在进行拼接,建议StringBuffer;不可变的String可以用来共享资源
由于java可以自动gc所以在对字符串重新赋值的时候并不会放在那里不去使用,导致内存泄漏;
虽然string是共享的但是,通过substring和+操作过的字符串并不共享,因此不要使用==来判断两个字符串是否相等,==是用来比较地址是否相同,应使用equals;也可以使用compareTo去判断是否一致(if(x.compareTo("ggg")==0)
空串是一个java变量,长度为0,内容为空;String还可以存放一个特殊的值null,表示目前没有任何对象与该变量关联。
关于码点与代码单元https://blog.csdn.net/so_geili/article/details/105477780?spm=1001.2101.3001.6650.3&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-3.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-3.pc_relevant_default&utm_relevant_index=4
String中的方法
blank()如果字符串为空或由空格组成返回true
trim()删除头与尾的U+0020字符
strip()删除头与尾的空格
在构建字符串时使用StringBuilder
StringBuilder sb=new StringBuilder();
sb.append(str 或ch)
因为输入是可见的,所以scanner不适用于从控制台读取密码,可以使用Console类来实现;只能在控制台使用,不能在ide使用
Console cons=System.console();String username=cons.readLine("user name:")char[] psw =cons.readPassword("psw:")
不能读取单个单词或数值;
Scanner类中的方法
hasNext():检测输入中是否还有其它单词;
2.格式化输出
https://blog.csdn.net/jhsword/article/details/108574442
3.文件的输入与输出
https://www.cnblogs.com/glam/p/11877640.html
4.控制流程
块作用域:是若干java语句组合成的,使用大括号包括起来,一个块可以嵌套在另外一个块中但是两个嵌套的块中的变量不能相同;
循环
do{}while()会使语句至少执行一次
由于0.1不能被二进制准确的表示,所以不要在判断语句中使用浮点类型(for(doube i=0;;;))
多重选择(switch语句)
switch(c){ case 1: ... break; default: ... break;}
case标签可以是char、byte、short、int、枚举、字符串
switch(x.toLowerCase()){ case "yes": ... break;}Size sz=...//枚举switch(sz){ case SMALL://不用使用Size.SMALL ... break;}
int n;read_n:while(true){....break read_n;//跳到指定位置}
大数(bigInteger,bigDecimal)
可以用于提升精度;
使用静态的valueOf方法可以将普通的数值转换为大数
BigInteger a=BigInteger .valueOf(100);
对于更大的数可以使用一个带字符串参数的构造器
BigInteger a=new BigInteger (“”3333333333333333333332323233);
大数不能使用+与*只能使用add与multiply方法实现+与*方法;
java只是为String方法重载了+运算符
add()、subtract()、multiply()、divide()、mod()和、差、积、商及余数方法
sqrt()、compareTo()、valueOf(long x)平方根、相等为0,小于为负数,大于为正数、返回等于x的大整数
一旦创建了数组的大小就不能改变,若经常改变可以用数组列表;
可以使用匿名数组去重新初始化一个数组而不用创建新的变量
在java中允许有长度为0的数组
int[] a=new int[100];//声明并初始化了一个可以存储100个整数的数组int[] a={1,2,3,};//创建数组对象并同时提供初始值的简写形式,最后一个值后面允许有逗号,方便添加值new int[]{1,2,3,4};//匿名数组new a[0]//长度为0的数组new a[]{}
for each循环
foreach循环的是每一个元素,而不是下标;使用for each 的必须是数组或者是实现了Iterable接口的类对象(arraylist);tips(arrays.toString(a)会将全部元素打印出来)
int[] a=new int[100];for(int element:a){sout(element);}
数组拷贝
在java中允许一个数组变量拷贝到另一个数组变量。这时两个变量将使用同一个数组;
数组值的拷贝时,第二个参数可以用来增加数组大小,如果数组元素时数值型额外的元素将默认为0;如果长度小于需要拷贝的长度则只拷贝前边的;
java中的【】运算符被预定义为会完成越界检查,而没有指针运算,即不能通过a+1得到数组的下一个元素
int[] luckyNumber=smallPrimes;luckyNumber[5]=12;//smallPrimes[5] is also 12int[] copiedLuckyNumbers=Arrays.copyOf(luckyNumbers,luckyNumbers.length);//将一个数组中的所有值拷贝到一个新的数组中
命令行参数
main主函数的String[] args是用来存命令参数的
如果在控制台使用java 类名 -g hh
那么在输出args这个数组就会有 -g hh
程序名是不会存在数组中的
数组排序
Arrays.sort(数组)
随机数
Math.random会返回一个0-1之间的随机浮点数如果Math.random*n会返回一个0-(n-1)之间的一个随机数
数组中的方法
binarySearch(xxx[] a,xxx v)
binarySearch(xxx[] a,int start,int end,xxx v)使用二分法查找值找到返回下标,反之为负数;
fill(xxx[] a,xxx v)将数组所有数据设置为v
多维数组(本质是一维数组)
输出时需要两个循环嵌套
for(double[] row:a) for(double value:row)Arrays.deepToString(a)//用于输出二维数组
通过采用面向对象的设计风格,更容易去解决一些bug,因为在一个对象中可能包含20个方法,比在过程中的方法要少很多;
面向对象是数据优先,算法其次;
由类构造对象的过程称为创建类的实例;
对象的三个特性(行为、状态、标识)
行为:可以对对象完成什么操作;
状态:调用那些方法时,对象会如何响应;对象状态的改变必须通过方法来调用,如若不用则破坏了封装;对象的状态是会影响对象的行为的;
标识:如何区分具有相同行为与状态的不同对象;对于同一个类的实例之间的标识是不同的,对应的状态也应该是有差异的;
依赖:一个类的方法使用或操纵另一个类的对象;应该尽量减少依赖(耦合)
聚合:类A的对象包含类B的对象
UML图
构造函数https://blog.csdn.net/special00/article/details/82793367
1、构造函数可以通过instanceOf来识别对象;但是每次实例化一个对象,都会把属性和方法赋值一遍,方法一样地址重新开辟,内存浪费;
2、自认为构造函数应该就是用来初始化与创建实例的;
3、在对一个类进行实例化引用的时候可以这样:
new 类名()调用方法可以new 类名().方法();
但是这样每一次使用都会创建新的对象,所以可以这样
类名 别名=new 类名();
对象的变量并不会包含一个对象,仅仅是引用一个对象;
Date a=new Date();此时的a并不包含Date对象,而是引用了Date对象;
所有的java对象都存储在堆中,当一个对象包含另一个对象变量时,他只是包含着另一个堆对象的指针;
最好使用不同的类表示不同的概念
调用方法后会更改对象的状态称为更改器方法
只访问对象而不更改对象称为访问器方法
日期类
static LocalDate now()//返回当前日期对象static LocalDate of(int year,int month,int day)//构造一个给定日期对象LocalDate plusDays(int n)//生成当前日期之后n天的日期 LocalDate minusDays(int n)//生成当前日期之前n天的日期
import java.time.LocalDate;public class Employee { private String name; private double salary; private LocalDate hireDay; public Employee() { } public Employee(String name, double salary, int y,int m, int d) { this.name = name; this.salary = salary; this.hireDay = LocalDate.of(y,m,d); } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getSalary() { return salary; } public void setSalary(double salary) { this.salary = salary; } public LocalDate getHireDay() { return hireDay; } public void setHireDay(LocalDate hireDay) { this.hireDay = hireDay; }}
在java中只能有一个公共类且与类名相同,可以有任意数目的非公共类;、
在一个类编译时如果发现使用了其他类,便会自动将其他类编译,如果其他类发生了版本上的更新,编译器也会重新自动的编译(java中make功能)
private 关键字修饰的字段,只能在本类中使用,但是可以开放一个公共的get/set来对这个属性进行操作,建议将实例的字段全部设置为private;
构造器
1、构造器与类名同名
2、每个类可以有多个构造器
3、构造器参数可以有多个
4、构造器没有返回值
5、构造器总是伴随着new操作符一起调用
6、不能对一个已经存在的对象调用构造器来达到重新设置实例字段的目的,会发生编译错误
所有的java对象都是在堆中构造的;
不要在构造器中定义与实例字段同名的局部变量;
JAVA10中可以使用var声明局部变量,甚至可以var a=new 类名();
如果一个字符串不允许设置为null则可以:
public Employee(String name, double salary, int y,int m, int d) { Objects.requireNonNull(name,"name cannot be null"); this.name=name; this.salary = salary; this.hireDay = LocalDate.of(y,m,d); }
隐式参数与显式参数
显式参数是在方法括号里出现的;
隐式参数可以用this指代,可以将实例字段与局部变量明显的区分开来;
不要编写返回可变对象引用的访问器方法。
class Test{ private Date hireDay;public Date getHireDay(){ return hireDay;}}//这是不对的class Test{ private Date hireDay;public Date getHireDay(){ return (Date)hireDay.clone();//如果返回一个可变对象的引用,首先应该对他进行克隆}
final修饰符
被final修饰的字段应该在构造器执行之后,这个字段已经被初始化了,以后也不能修改,所以无set方法;
final修饰符对于类型为基本类型或者不可变类型的字段最有用(String)
当final修饰一个可变的类时,会使得当前被修饰的实例不会在指向另外一个StringBuilder对象,但是这个对象还可以更改
private final StringBuilder eva;eva=new StringBuilder();eva.append()//可以更改
如果将一个字段定义为static,每个类只有一个这样的字段。而对于非静态的实例字段,每个对象都有自己的一个副本。(即一个类中有一个static修饰的字段,还有一个非static修饰的字段,当创建了若干个该类的实例,会有若干个非static的字段,但是static修饰的字段只会有一个。
即使没有类的实例,static修饰的字段也是存在的,因为它是跟随类而存在的又称类字段;
harry.id=Employee.nextId;
Employee.nextId++;//harry的id字段被设置为静态字段nextId当前的值,静态字段加一
静态方法
静态方法是不在对象上执行的方法。没有隐式参数;
静态方法不能访问非static字段,因为它不能在对象上执行操作;
可以使用对象调用静态方法,但是不建议,因为使用对象调用的静态方法,所获得的东西与当前对象无关,建议使用 类名.静态方法();
静态方法的使用情况:
1、方法不需要访问对象状态,因为他需要的所有参数都通过显示参数提供(例如:Math.pow(2,3)不需要使用对对象状态访问获取;
2、方法只需要访问类的静态字段;
工厂方法
静态方法的另一个用途,使用静态工厂方法来构造对象;
Test a =Test.getF();Test b=Test.getG();double x=10;sout(a.method(x))//$10sout(b.method(x))//10%
为什么不利用构造器去重载呢?
1、无法对构造器重命名;当想要两个完全不同的方法名是不可以的
2、使用构造器,无法改变所构造对象的类型;想要返回一个类的子类是不可以的
main方法
main方法不对任何对象进行操作。事实上,在启动程序时还没有任何对象。静态的main方法将执行并构造程序所需的对象。
static
static
如果obj不为null则返回obj,反之,返回默认对象
java中总体是使用值传递
值传递:将方法外的变量的值拷贝一份给方法的入参;
方法不能修改传递给他的任何参数变量的内容;
int i=3;public void t(int x){ x*=3;}//并不会修改i的值
java程序设计语言对对象采用的不是按引用调用,实际上,对象引用是按值传递的。
方法参数能做什么和不能做什么:
1、方法不能修改基本数据类型的参数
2、方法可以改变对象参数的状态
3、方法不能让一个对象参数引用一个新的对象
默认字段初始化
在一个方法中,局部变量必须明确初始化,但是在类中,没有初始化,会默认初始化为默认值;
无参构造
如果一个类没有编写构造器,会有一个默认的无参构造器,在无参构造里会将实例字段设置为默认值,如果一个类中设置了一种有参的构造器,那么不会默认构造一个无参构造,在实例化类时也不能使用无参的方法构造(T a=new T())
显示字段的初始化
在字段进行初始化时,并不是一定要直接进行赋值来进行初始化,也可以用方法进行初始化;
class Test{private int age=ageAdd();private static int ageAdd(){int i=1;i++;return i;}
调用另一个构造器
关键字this指示一个方法的隐式参数,也可以用于调用同一个类的另外一个构造器;
public Test(double s){ this(""+id,s) id++;}
这将会在使用new Test(2220)时,调用Test(String,double)构造器;
在进行初始化时还可以使用如下两种
{//在代码块里初始化}static{//在静态代码块里初始化}
包是将类组织在一个集合中,可以将自己的代码与他人的代码分开管理;
包名
com.horstmann.corejava.Test
域名。工程名。类名
如果导入的两个包中包含名称相同的类,会有错误提示;
如果两个包都是用其中名称相同的方法的话,在使用时将报名打完整;
var day=new java.sql.Date();
可以静态导入包的静态方法和字段,而不只是类,在使用时不必加类名前缀
import static java.lang.System.*//还可以指定方法或字段out.println("")
编译器在编译源文件的时候不检查目录结构。
假设有一个源文件开头有以package com.mycompany;
即使这个源文件不在这个子目录下,也会编译成功(不依赖其他包),但是不能运行;
类的路径必须与包名匹配;
类文件可以存储在jar文件中
创建jar文件
jar的制作工具在jdk/bin目录下
指令jar cvf jarFileName file1 file2
文档注释
类注释必须放在import语句之后,类定义之前
类的设计技巧
继承:基于已有的类创建新的类。继承已存在的类就是复用其方法的同时还可以增加一些新的方法和字段
在一个公司里有职位的不同,但是本质是员工,员工只是领基本工资,经理是可以有奖金的,所以,创建一个员工类,里面包含了工资的方法及字段,这时经理需要创建一个新的类,去继承员工类,并且自己有奖金,所以在经理类中写一个奖金的字段及方法;
java中的继承都是公共继承,公共继承是将public与保护修饰的字段和方法继承下来;
子类拥有的功能要比父类多得多,父类的方法少但是是最基础方法;
在子类中是不能调用父类的私有变量的,但是可以通过父类的get方法去调用父类的私有变量,
在子类中使用,但是如果在父类中调用的方法名与子类中的方法重复,就会造成反复的调用子类的方法,致使程序崩溃i,但是可以使用super来区别,使用super可以用来专门调用父类的方法;
class A{private int f;public int getF(){return f;}class B extends A{public int getF(){return super.getF()+1;}
super关键字
super关键字不是一个对象的引用,不能将值super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字;https://www.cnblogs.com/wzd19901129/p/14382861.html
https://www.cnblogs.com/wzd19901129/p/14382861.html
父类 别名=new 子类
通过父类可以引用不同的子类
对于向上转型只能访问父类中有的方法和属性,对于子类中有但是父类中没有的不能使用,尽管是重载该方法;若子类重写了父类中的某些方法,在调用该方法的时候,必定是使用子类中定义的这些方法(动态链接,动态调用)https://www.cnblogs.com/chenssy/p/3372798.html
方法调用
tips:在覆盖方法的时候,返回值类型不是签名(方法的名字和参数列表)的一部分,允许子类覆盖的方法返回类型改为子类;
tips:动态绑定有一个非常重要的特性:无需对现有的代码进行修改就可以对程序进行扩展。
动态绑定
阻止继承:final类和方法
如果以jvm加载了另一个类,这个子类覆盖了一个内联方法,那么优化器将会取消对这个方法的内联;
强制类型转换
在进行强制类型转换的时候,小数强制转为int的时候会舍弃小数后的;
在进行向下转型时,将一个父类引用赋给一个子类变量时需要去验证类型是否可以成功转换
if(staff[1] instanceof Manager){boss=(Manager) staff[1]}//Manager boss=(Manager) staff[1];errorString c=(String) staff[1];会编译错误,这是因为String不是Employee的子类
tips:尽量少使用强制类型转换和instanceof运算符
抽象类
尽量建议将通用的字段和方法放在父类;
不包含抽象方法也可以定义为抽象类;
抽象类不能实例化;
在抽象父类定义一个getA()方法,此时在子类继承时需要实现,然后可以根据 抽象父类 p=new 子类();p.getA();但是抽象父类没有这个方法的话,就不能使用p去调用;除非不使用抽象父类的所有东西;
受保护访问
在一个类中最好将字段写为private,将方法写为public,但是在有些时候希望父类的方法只允许子类使用就可以将方法修饰为protected保护类型;
建议:方法使用这个,字段不要用;
privat:仅对本类可见
public:完全可见
protected:对本包及其子类可见
默认:对本包可见
Object类时java中所有类的祖先;
Object类型的变量
可以使用Object类型的变量引用任何类型的对象,但是想要具体进行操作就需要强制转型;
equals方法
用于检测一个对象是否等于另外一对象。
在实际应用时,为了防备某两个字段的值可能为空的情况,则需要改写成
return Objects.equals(name,other.name)&&salary==other.salary&&Objects.equals(hireDay,othser.hireDay);
在子类中定义equals方法时,首先调用父类的equals。如果检测失败,对象就不可能相同,如果父类中的字段都相同,就需要比较子类中的实例字段。
public class A extends B{public boolean equals(Object otherObject){if(!super.equals(Object)) return false;A a=(A)otherObject;return bonus=other.bonus;}}
相等测试与继承
java语言规范要求equals方法具有下面的特性
如果子类可以有自己的相等性概念,则对称性需求将强制使用getClass检测;
如果由父类决定相等性概念,那么就可以用instanceof检测,这样可以在不同子类的对象之间进行相等性比较;
如何编写一个equals?
如果在子类中重新定义equals,就要在其中包含一个super.equals(Other)调用。
在重写一个父类方法时最好加一个注解:@Override
hashCode方法
不同的对象会有不同的散列码,且散列码是没有规律的,但是如果两个对象引用同一个字符串内容会时相同的散列码;
最好使用null安全的方法Objects.hashcode,如果其参数为null会返回0
toString方法
object中的toString方法一般会将类名直接编码到里面,但是不太好,可以将类名硬编码到方法中改成getClass.getName();这样toString方法也可以由子类调用,如果父类调用了getClass.getName()那么,子类只需要super.toString();
数组的toString:Arrays.toString();多维数组:Arrays.deepToString()方法
强烈建议:为每一个类都加一个toString
getCalss():返回包含对象信息的类对象。
ArrayList
声明数组列表
数组列表的类型不能时基本类型,应该是包装类
ArrayList
会动态的增加空间容量;
如果知道需要用多大空间,可以使用list.ensureCapacity(int num);将分配num个空间,
ArrayList
通过使用size方法获取list长度,list.size();
如果知道数组的长度不会改变了,使用lsit.trimToSize()方法去然后去让gc回收多余的空间。
访问数组列表元素
使用get(int num)去获取一个值
使用set(int num,object args)去设置一个值
使用remove(int n)去移除一个值
ArrayList<> list=new ArrayList();whuile(){x=...;list.add(x);}var a=new X[list.size()];list.toArray(a);//将数组元素拷贝到一个数组中
类型化与原始数组列表的兼容性
@SuppressWarning("unchecked") ArrayList list=....//这个注解就是在警告不太重要的时候,加上
有时需要将基本类型转为对象。所有的基本类型都有一个与之对应的类。
包装器类包括:Integer,Long,Float,Double,Short,Byte,Character,Boolean前六个类派生于公共的父类Number;
自动装箱:list.add(3)---->list.add(Integer.valueOf(3))向ArrayList
自动拆箱:int n=list.get(i)--->int n=list.get(i).intValue()将一个Integer对象赋值给一个int值
如果对一个Integer对象的值进行自增的话,编译器会将其自动拆箱,自增完,然后在装箱;
在对包装类进行比较时应该使用equals(介于-128到127之间的short于int可以用==比较,因为他们会包装到固定的对象中。
如果在一个表达式中用了Integer和Double,这会让Integer自动拆箱后提升为double在装箱成Double;
将字符串转换成整型可以int x=Integer.parseInt(s);
不能使用包装器去创建一个可变的参数,因为他是不可变的;
int intValue():将这个Integer对象的值作为一个int返回
Integer valueOf(String s):返回一个新的Integer对象
Number parse(String s):返回数值
printf(String fmt,Object... args)
这个方法接受两个参数,一个是格式字符串,另一个是Object【】数组,其中如果传的是基本类型则会自动转成包装类
System.out.printf("%d %s",new Object[]{new Integer(1),"widgets"});
因为枚举类型定义声明的是一个类,已经被实例,所以不可能构造新的对象,因此在比较两个枚举类型的值时不用equals用==就可以
public enum Size{ SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRGE("XL"); private String abb; private Size(String abb){this.abb==abb);} public String getAbb(){return abb;}}
在枚举类型里可以有字段、方法、构造方法,构造器只是在构造枚举常量的时候调用。构造器必须时私有的;
Size.SMALL.toString(),将返回字符串“SMALL"
Size s=Enum.valueOf(Size.class,"SMALL");
将s设置为Size.SMALL
Size[] values=Size.values();
返回一个包含所有枚举值的数组
Size.SMALL.ordinal()会返回small的下标
待补充
接口:用来描述类应该做什么,而不指定他们具体做什么;接口不是类;
接口中所有的方法都自动是public方法;
接口可以定义常量;
接口不会有实例字段;java8以后可以提供简单方法,这些方法不能引用实例字段;
提供实例字段和方法实现的任务应该由实现接口的那个类来完成,因此可以将接口看作是没有实例字段的抽象类。
在对接口实现的类一定要设置成公开,否则会默认为默认类型;
(tips:在用equals对比大数时,精度不同也不同)
为什么不能在一个类中直接实现comparto方法而是要实现Comparable接口呢?
因为java时一个强类型语言,在调用方法时,编译器要能检查这个方法确实存在。
public class Employee implements Comparable{ private String name; private double salary; public Employee (String name,double salary){ this.name=name; this.salary=salary; } public String getName() { return name; } public double getSalary() { return salary; } public void raiseSalary(double byPercent){ double raise=salary*byPercent/100; salary+=raise; } @Override public int compareTo(Employee o) { return Double.compare(salary,o.salary); }}
方法:
接口的属性
不能构造接口的对象,但是可以声明接口的变量;
可以使用instaneof检查某个类是否实现了某个接口;
接口中的方法自动设置为public,字段设置成public static fianl;
接口与抽象类
因为继承不能多继承,所以引入了接口概念;
静态和私有方法
在java8中,允许在接口中增加静态方法
默认方法
public interface Comparable{ default int compareTo(T other){return 0;}}
默认方法必须用default修饰;
默认方法的重要用法是接口演化:即在很长时间之前设置了一个类实现了一个接口,这时你想为这个接口增加一个新的接口方法,但是他不是默认方法,那么这个类将不能编译,因为没有实现这个方法。为接口增加一个非默认方法不能保证”源代码兼容“。如果增加了默认,则既可以编译又可以调用这个方法;
解决默认方法冲突
Class Student implements Person,Named{ public Stirng getName(){ return Person.super.getName();}}interface Person{ default String getName(){return "";}}interface Named{ default String getName(){return getClass().getName()+"_"+hashCode();}}
千万不要让一个默认方法重新定义Object类中的某个方法;
接口与回调
回调(Callback)时一种常见的程序设计模式。
Comparator接口
当希望根据自己的意愿去对某个东西进行自己排序可以实现Comparator接口去修改compare方法
public interface Comparator{ int compare(T first,T second)}
对象克隆
待补充
为什么引入lambda表达式
lambda表达式是一个可传递的代码块,可以执行一次或者多次;
compaer方法不是立即调用。实际上,在数组完成排序之前,sort方法会一直调用compare,只要元素顺序不对就会重新排列;
lambda表达式的语法
(String a,Stirng b)-> { if(a.length()b.length())return 1; else reutn 0;}Comparator comp//不加类型 =(a,b) ->a.length()-b.length();
函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式,即函数式接口;
为什么函数式接口必须要有一个抽象方法?
因为当接口重新声明Object类的方法,这些声明可能会让方法不在是抽象的。接口可以声明非抽象的方法;
最好把lambda表达式看作是一个函数而非对象,lambda表达式可以传递到函数式接口;lambda表达式可以转换为接口;
(tips:不能将lambda表达式赋值给类型为Object的变量,因为它不是函数式接口;)
方法引用
方法引用也不是一个对象,但是,为一个类型为函数式接口的变量赋值时会生成一个对象。
表达式 System.out::println就是一个方法引用
var timer=new Timer(1000,event->System.out.println(event));||var timer=new Timer(1000,System.out::println);
方法引用的格式
类型or类名在前方法在后面;
https://www.cnblogs.com/wuhenzhidu/p/10727065.html
引用方法示例
this::equals==》x->this.equals(x).使用super也是合法的。
变量作用域
lambda表达式有三部分:
lambda表达式不能引用会改变的值;不能有同名的局部变量
处理lambda表达式
lambda的重点是延迟执行
之所以希望之后执行代码是因为:
内部类是定义在另一个类中的类。
为什么要使用?
内部类对于构建代码还是很有用的。
内部类待补充。。。
待补充。。。
利用代理可以在运行时创建实现了一组给定接口的新类。只有在编译时期无法确定需要实现哪个接口时才有必要使用代理。
想要创建一个代理对象需要使用Proxy类的newProxyInstance方法。这个方法有三个参数:
使用代理的目的:
代理类是在程序运行中动态创建的,一旦创建,他们就成了常规类,与虚拟机中的其他类无区别。
代理类的特性?
isProxyClass方法用来检测一个特定Class对象是否表示一个代理类。
在java程序设计语言中,异常对象都是派生于Throwable类的一个实例类;
在Throwable类下分为error与exception两大类
在exception下又分为IOException和RuntiemException异常;
运行异常包括以下问题:
不是运行异常包括:
如果出现运行异常就一定是,自己问题。
声明检查型异常
什么时候用到throws?
void drawImage(int a)throws ArrayIndexOutOfBoundsExceptionthrow new EOFException();
创建异常类
class FileFor extends IOException{//自己创建异常 public FileFor(){} public FileFor(String gripe){ super(gripe); } }可以直接throws也可以throw new FileFor();
try{ }catch(){}catch(){}catch(){}finally{}
再次抛出异常与异常链
try{access the database}catch(SQLException o){var e=new ServletException("database error");e.initCause(o);//将原始异常设置为新异常的原因throw e;}捕获到这个异常的时候,可以使用Throwable o=caughtException.getCause();来获取原异常
使用这种技术,可以在子系统中抛出高层异常,而不会丢失原始异常的细节;
检查型异常可以使用这种技术将其包装成运行时异常;
finally中的return会吞掉其他的return的值
(eg:在try中return 3;在finally中return 5;会输出5;并且在try中出现的异常,finally中出现return会吞掉异常)
分析堆栈轨迹元素
https://blog.csdn.net/qq_34115598/article/details/79855271
https://www.cnblogs.com/hujingwei/p/5147236.html
基本日志
全局日志记录器的调用
Logger.getGlobal().info("打开文件");
在默认情况下,会打印
May 10,2022 20:33:22pm LoggingImageViewer fileOpen
INFO:打开文件
但是在main之间调用Logger.getGlobal().setLevel(Level.OFF);会取消所有日志
日志的内容
过滤器
同一时刻最多只能有一个过滤器;
意味着编写的代码可以对多种不同类型的对象重用。
泛型类就是一个或多个类型变量的类;
泛型类可以有多个类型的变量。其中第一个字段和第二个字段可以是不同的类型;
public class Pair
tips:常见的做法是类型变量使用大写字母,而且很简短。E表示集合的元素类型,K,V
表示键和值的类型,T表示任意类型
泛型方法可以在普通类中定义,也可以在泛型类中定义
class ArrayAlg{ public static T getMiddle(T...a){ return a[a.length/2]; }}Stirng middle=ArrayAlg.getMiddle("a","fdf","fss");//可以有这两种方式String middle=ArrayAlg.getMiddle("a","fdf","fss");//可以有这两种方式
类型变量的限定
为什么要用类型变量的限定?
当一个T不知道是否有compareTo方法,这时可以使用类型变量的限定,限制T只能是实现了Comparable接口 public static
T是限定类型的子类型;T和限定类型可以是类,也可以是接口。
一个类型变量或通配符可以有多个限定(eg:T extends Comparable&Serializable)
限定类型用&分割,而逗号用来分隔类型变量;
在java的继承中,可以根据需要拥有多个接口超类型,但最多有一个限定可以是类。如果有一个类作为限定,它必须是限定列表中的第一个限定。
jvm没有泛型类型对象——所有对象都属于普通类;
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(去掉类型参数后的泛型类型名)。类型变量会被擦除,并替换为其限定类型。
public static T min(T[] a)...在没有限定类型时public static T min(T[] a)...中的private a==private object a但是有了限定后就是 private Comparable a;
tips:为了高效,应该在进行限定时将没有方法的接口放在列表的最后。public static
Pair budd=...;Employee buddy=budd.getA();
对原始方法Pair.getA()的调用
将返回的object类型强制转换为Employee类型
对于java泛型的转换,需要记住:
如果一个方法继承了另外一个方法,覆盖了方法,然后由于类型擦除导致两个方法相同,以至于可能会调用错方法,会有一个桥方法
public void setSecind(Object second){setSecind((LocaDate)second);}
待补充
不能用int只能用Integer
Pair a=...;Pair b=...;if(a.getClass()==b.getClass())true
因为getClass方法总是返回原始类型。他们比较时返回的都是Pair.class,所以为true
Pair table=new Pair
不能用new Pair
不能在类似new T(。。。)的表达式中使用类型变量;
eg:public Pair(){first=new T();second=new T();}//这是错误的,因为类型擦除后,T
会变成Object,肯定不会去调用new Object();
在java8后,最好的解决办法是让调用者提供一个构造器表达式
eg:Pair
makePair方法接收的是一个Supplier
String[] names=ArrayAlg.minmax(String[]::new,"Tom","ddd","dsaf");
private static T ggg;//这是错误的
在泛型中是不存在父类子类的继承,即使是父类子类
https://www.cnblogs.com/minikobe/p/11547220.html
https://www.cnblogs.com/keatkeat/p/3896448.html
在java类库中,集合类的基本接口是Collection接口。
public interface Collection{ boolean add(E element); Intertor iterator(); ...}
Iterator接口包含4个方法:
public interface Iterator{ E next();//访问下一个元素 boolean hasNext();//判断是否还有下一个元素 void remove();//移除 default void forEachRemaining(Cconsumer super E> action);}//输出元素Collection c=...;Iterator iter=c.iterator();while(iter.hasNext()){ String element=iter.next(); do something}//使用foreach输出元素for(String element:c){}//输出iterator.forEachRemaining(element->do something);
foreach循环可以处理任何实现了Iterable接口的对象
访问元素的顺序取决于集合类型。如果迭代处理一个ArrayList,迭代器将从索引的0开始;如果迭代的是HashSet会随机顺序去输出;
remove方法将会删除上次调用next方法时返回的元素,如果调用remove之前没有调用next是不合法的。
int size()boolean isEmpty()boolean contais(Object obj)//如果集合包含这个元素返回trueboolean contaisAll(Collection> c)boolean equals(Object other)boolean addAll(Collection extends E> from)boolean remove(obj)boolean removeAll(Collection> c);void clear()boolean retainAll(Collection> c)//删除所有与c不同的元素Object[] toArray() T[] toArray(T[] arrayToFill)default boolean removeIf(Predicate super E> filter)//用于删除满足某个条件的元素
集合有两个基本接口:Collection和Map
Collection用add添加元素,使用迭代器遍历元素、取值
Map用put(k,v)添加元素,使用get(k)遍历、取值
List是一个有序集合,元素增加到容易中的特定位置
tips:为了避免对链表完成随机访问操作,,java引入一个标记接口RandomAccess。
这个接口不包含任何方法,不过可以用它测试一个特定接口是否支持高效的随机访问
if(c instanceof RandomAccess){}else{}
Set接口等同于Collection接口,不过其方法的行为有更严格的定义。不允许添加重复元素;
集合类型 | 描述 |
---|---|
ArrayList | 可以动态增长和缩减的一个索引序列 |
LinkedList | 可以在任何位置高效插入和删除的一个有序序列 |
ArrayDeque | 实现为循环数组的一个双端队列 |
HashSet | 没有重复元素的一个无序集合 |
TreeSet | 一个有序集 |
EnumSet | 一个包含枚举类型值的集 |
LinkedHashSet | 一个可以记住元素插入次序的集 |
PriorityQueue | 允许高效删除最小元素的一个集合 |
HashMap | 存储k/v关联的一个数据结构 |
TreeMap | k有序的一个映射 |
EnumMap | k属于枚举类型的一个映射 |
LinkedHashMap | 可以记住k/v添加次序的一个映射 |
WeakHashMap | v不会在别处使用时就可以被gc的一个映射 |
IdentityHashMap | 用==而不是用equals比较k的一个映射 |
在数组中删除or增加一个元素的开销很大,因为在数组中删除or增加会让当前位置后的所有数据的下标+-1,造成时间浪费。
LinkedList类
var staff=new LinkedList();staff.add("ss");//添加一个staff.add("sssw");//Iterator iter=staff.iterator();//迭代器String first=iter.next();//访问第一个iter.remove()//移除最后一次访问的元素
链表是一个有序集合,因为链表的添加总是添加到链表的最后,如果想要添加到某个中间的位置就要依靠迭代器
interface ListIterator extends Iterator{ void add(E element);}//这个是默认add操作总会改变链表他还有两个方法previous()访问上一个hasPrevious()是否还有上一个
LinkedList类的listIterator返回了一个实现了ListIterator接口的迭代器
LinkedList linkedList=new LinkedList(); linkedList.add("sss"); linkedList.add("lll"); ListIterator iterator=linkedList.listIterator(); iterator.next(); iterator.add("kkk"); for (String c:linkedList ) { System.out.println(c); }//会在第二个元素之前将kkk添加进去
set方法用一个新元素替换调用next和previous方法返回的上一个元素
LinkedList linkedList=new LinkedList(); linkedList.add("sss"); linkedList.add("lll"); ListIterator iterator=linkedList.listIterator(); iterator.next(); //iterator.add("kkk"); iterator.set("kkk"); for (String c:linkedList ) { System.out.println(c); }//在输出时,会将sss替换为kkk
可以为一个集合关联多个迭代器,但是只能读;或者可以在关联一个可以读和写同时的一个迭代器;
tips:对于并发修改的检测有一个奇怪的例外。链表只跟踪对链表结构性的修改,例如添加和删除,但是set方法不被视为结构性修改。可以为一个链表链接多个迭代器,都去调用set方法修改现有链接的内容。
链表不支持快速随机访问。如果要查看第n个元素就必须从头开始,查看n-1个元素。
链表有一个get(你)方法,去查找特定位置的元素,但是linkedlist对象没有缓存位置,所以不要使用这个方法去遍历。
如果有一个整数索引n,list.listIterator(n)将返回一个迭代器,这个迭代器指向索引为n的元素前面的位置,也就是说next和lsit.get(n)会返回同一个元素。
LinkedList a=new LinkedList(); a.add("a1"); a.add("a2"); a.add("a3"); LinkedList b=new LinkedList(); b.add("b1"); b.add("b2"); b.add("b3"); ListIterator aIterator=a.listIterator(); Iterator bIterator=b.iterator(); while (bIterator.hasNext()){ if (aIterator.hasNext()) aIterator.next(); aIterator.add(bIterator.next()); } System.out.println(a); bIterator=b.iterator(); while (bIterator.hasNext()){ bIterator.next(); if(bIterator.hasNext()){ bIterator.next(); bIterator.remove(); } } System.out.println(b); a.removeAll(b);//将a中含有的b的元素全部移除 System.out.println(a);
在不需要同步的时候用ArrayList,在需要同步的时候使用Vector;
快速查找对象 可以用散列表,散列表为每个对象计算一个整数,称为散列码 。
散列表用于存储没有重复的元素;如果元素的散列码发生了改变,元素在数据结构中的位置也会发生改变。
树集是一个有序集合,可以以任意顺序将元素插到集合中。
在实现数集时,需要确定这些元素实现了Comparable接口或者构造一个;
如果没有必要排序就没必要使用树集导致时间过长;
队列允许你高效的在队尾添加,在队头删除,不支持在对列中间添加元素
ArrayDeque LinkedList
优先队列中的元素可以按照任意的顺序插入,但会按照有序的顺序进行检索。
即无论何时调用remove()总会获得当前优先队列中最小的元素。
优先队列并没有对所有元素进行排序。
优先队列典型用法是任务调度。每一个任务有一个优先级,每当启动一个新的任务时,都会将优先级最高的任务从队列中删除。
java库为映射提供了两个通用的实现:HashMap和TreeMap,这两个类都实现了Map接口
HashMap hashMap=new HashMap<>(); hashMap.put(1,"q"); hashMap.put(2,"w"); hashMap.put(3,"t"); hashMap.forEach((k,v)-> System.out.println("key: "+k+" value: "+v));
对相同单词出现次数的计数
counts.put(word,counts.getOrDefault(word,0)+1);如果一个单词第一次存入会是null,所以用get这个方法解决。
另一种方法是:
counts.putIfAbsent(word,0)//只有当建原先存在时才会放入另一个值
counts.put(word,counts.get(word)+1);
Set
Collection
Set
tips:keySet不是HashSet和TreeSet,而是实现了Set接口的另外某个类的对象。Set接口扩展了Collection接口,因此可以像使用任何集合一样使用keySet。
eg:可以枚举一个映射的所有键
Set keys=map.keySet();for(String key:keys){ do something}for(Map.Entry entry :staff.entrySet()){ String k=entry.getKey();Employee v=entry.getValue();do something}两种方式访问键值
HashMap map=Collections.synchronizedMap(new HashMap
只有在随机访问,二分查找才有意义
如果需要把一个数组转换为集合,List.of可以达到这个目的
String values=....;
var staff =new HashSet<>(List.of(values));
从集合转换为数组
staff.toArray(new String[staff.size());
public void a(Collection- items){ for(Item item:items){ }}//这样可以接受各种类型的集合
实现Runnable接口public interface Runnable{ void run();}Runnable r=()->{code};var t=new Thread(r);t.start();继承Thread类 class MyThread extends Thread{ public void run(){ code }}
New | 新建 |
Runnable | 可运行 |
Blocked | 阻塞 |
Waiting | 等待 |
Timed waiting | 计时等待 |
Terminated | 终止 |
要确定一个线程的当前状态,只需要调用getState方法。
当使用new操作符创建一个新线程时,这个线程还没开始运行。处于新建状态
一旦调用start方法,线程就处于可运行状态。一个可运行的线程可能正在运行也可能没有运行。
现在所有的桌面以及服务器操作系统都使用抢占式调度。但是,像手机这种小型设备可能使用协作式调度。在这样的设备中,一个处理器只有在调用yield方法或者被阻塞或等待时才会失去控制权。
static void yield():使当前正在执行的线程向另一个线程交出运行权。
当线程处于阻塞或等待状态时,它暂时是不活动的。不会运行代码,且消耗最少的资源。要由线程调度器重新激活这个线程。
线程会因为以下两个原因之一而终止:
当线程的run方法执行方法体中最后一条语句后在执行return语句返回时,或者出现了方法中没有捕获的异常时,线程将终止。
interrupt方法可以用来请求终止一个线程,当对一个线程调用了这个方法,就会设置线程的中断状态。这是每个线程都有的boolean标志。每个线程都应该时不时的去检查这个标志,以判断线程是否被中断。
while(!Thread.currentThread().isInterrupted()&&more work to do){ do more work}//检查是否设置了中断状态Thread.currentThread().isInterrupted()
但是,如果线程被阻塞就无法检查中断状态。当在一个被sleep或wait调用阻塞的线程调用interrupt方法时,那个阻塞调用将被一个InterruptedException异常中断。
没有任何语言要求被中断的线程应当终止。中断一个线程只是要引起它的注意。被中断的线程可以决定如何响应中断。更普遍的情况是,线程只希望将中断解释为一个终止请求。
使用sleep,isInterrupted检查没有必要,如果设置了中断状态,此时使用sleep,是不会休眠的。实际上,他会清除中断状态,并抛出InterruptedException。因此,如果你循环调用了sleep不要检查中断状态,而应该捕获InterruptedException异常。
Runnable r=()->{ try{ while(more work to do){ do more work Thread.sleep(delay); }}catch(InterruptedException e){sleep or wait; }finally{}};
tips:interruped方法是静态方法,他检查当前线程是否被中断。而且,这个方法会清除该线程的中断状态。isInterruped方法是一个实例方法,可以用来检测是否有线程被中断。
void mySubTask(){ (... try{sleep(delay);} catch(InterrupedException e){}//不要这样做 ...}
如果想不出在catch中可以做什么有意义的工作,可以有这两种选择:
void interrupt():向线程发送中断请求。中断状态将设置为true。如果当前该线程被一个sleep调用阻塞,则抛出InterruptedException异常;
static boolean interrupted():测试当前线程是否被中断。他会将当前线程中断状态设置为false;
可以通过调用t.setDaemon(true);将一个线程转换为守护线程。用于为其他线程提供服务。清空过时缓存项的线程也是守护线程。当只剩下守护线程时,jvm就会退出。
var t=new Thread(runnable)l
t.setName("名子");
线程的run方法不能抛出任何检查型异常,但是非检查型异常可能会导致线程终止,以至于线程死亡。
但是对于会传播的异常,在线程死亡之前会传递到一个用于处理未捕获异常的处理器。
这个处理器必须是一个实现了Thread.UncaughtExceptionHandler接口的类,这个接口只有一个方法 void uncaughtException(Thread t,Throwable e);
可以用setUncaughtExceptionHandler方法为任何线程安装一个处理器。也可以用Thread类的静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器。替代处理器可以使用日志API将未捕获的异常的报告发送到一个日志文件。
如果没有安装默认处理器,默认处理器则为null。但是,如果没有为单个线程安装处理器,那么处理器就是该线程的ThreadGroup对象。
tips:线程组是可以一起管理的线程集合。默认情况下,创建的所有线程都属于同一个线程组。
ThreadGroup类实现了Thread.UncaughtExceptionHandler接口。它的uncaughtException方法执行以下操作:
void setPriority(int newPriority):设置优先级
假设两个线程同时执行指令
accounts[to]+=amount;
这个指令会进行以下处理:
现在假定第一个线程执行步骤1,2;然后它的运行权被抢占。然后第二个线程被唤醒,更新account数组中的同一个元素。然后第一个线程被唤醒,完成3操作。这个动作会抹去第2个线程所做的更新。
ReentrantLock重入锁:因为线程可以反复获得已拥有的锁。锁有一个持有计数来跟踪对lock方法的嵌套调用。线程每一次调用lock后都要调用unlock来释放锁。由于这个特性,被一个锁保护的代码可以调用另一个使用相同锁的方法。
一般unlock都放在finally中;
ReentrantLock():构造一个重入锁,可以用来保护临界;
ReentrantLock(boolean fair):构造一个公平锁。但是慢。虽然是公平锁,但是在线程调度器选择忽略一个已经为锁等待很长时间的线程,他就没有机会得到公平处理。
可以使用newCondition方法获得一个条件对象
class Bank{ private Condition sufficientFunds; ... public Bank(){ ... sufficientFunds=bankLock.newCondition(); }}如果transfer方法发现资金不足,他会调用sufficientFunds.await();
当调用了await方法,就暂停当前线程,放弃锁,允许另一个线程执行。
只有当另一个线程完成某一个操作,调用了sufficientFunds.signalAll();才会从新激活这个等待的线程。
通常await调用应放在如下的循环中
while(!(ok to proceed))condittion.await();
因为他没有办法自行唤醒,所以需要其他线程去唤醒,如果没有线程唤醒就会导致死锁现象。如果所有其他线程都被阻塞,最后一个活动线程调用了await方法但没有先解除另外某个线程的阻塞,现在这个线程也会阻塞。此时没有线程可以解除其他线程的阻塞状态,程序会永远挂起。
应该什么时候调用signalAll呢?
只要一个对象的状态有变化,而且可能有利于等待的线程就可以调用。
public void transfer(int from,int to,int amount){ ankLock.lock(); try{ while(accounts[from]
使用条件很有挑战性,在使用条件对象之前,应该考虑使用同步代码块那部分内容;
锁和条件总结:
从java1.0版本开始,Java中的每个对象都有一个内部锁,如果一个方法声明synchronized关键字,那么对象的锁将保护整个方法,即想调用这个方法,就得获得内部锁。
synchronized中的wait或notifyAll等价于await和signalAll
将静态方法声明为同步也是合法的。如果调用这样一个方法,他会获得相关类对象的内部锁。
内部锁和条件存在一些限制:
有时程序员使用一个对象的锁来实现额外的原子操作,这种做法称为客户端锁定;
监视器可以不要求程序员考虑显式锁就可以保证多线程的安全。
特性:
因为java中每一个对象都有一个内部锁和一个内部条件。如果一个方法用synchronize关键字声明,那么,它表现得就像是一个监视器方法。但是,java对象在以下3个重要方面不同于监视器,这削弱了线程的安全性:
现代的处理器与编译器,容易出错:
编译器被要求在必要的时候刷新本地缓存来支持锁,而且不能不相应地重新排列指令顺序。
tips:如果写一个变量,这个变量可能会被另外一个变量读取,或者,如果读一个变量,而这个变量可能已经被另外一个线程写入值,那么必须使用同步。
假如,假设一个对象有一个boolean标记的done,它的值由一个线程设置,而由另外一个线程查询,可能你会将设置与查询用同步标签标记,但是很麻烦,不如直接使用volatile
private volatile boolean done;
但是,volatile变量不能提供原子性,不能确保翻转字段中的值。不能保证读取、翻转和写入不被中断。
final字段也能时共享字段,其他线程会在完成构造后才看到被final修饰的字段,但是这不是线程安全的,当有多个线程更改和读取时仍然需要同步。
当对共享变量除了赋值之外并不做其他操作,那么可以将这些共享变量声明为volatile。
AtomicInteger类提供了方法incrementAndGet,他们分别以原子的方式将一个整数进行自增或自减。
public static AtomicLong nextNumber=new AtomicLong();long id=nextNumber.incrementAndGet();//实现自增,且生成新的值时不会中断,可以保证即使时多个线程并发地访问同一个实例,也会计算并返回正确的值g假如,希望跟踪不同线程观察的最大值largest.updateAndGet(x->Math.max(x,observed));或largest.accumulateAndGet(observed,Math::max);如果预期可能存在大量竞争,只需要使用LongAdder而不是AtomicLong,需要调用increment让计数器自增,或者调用add来增加一个量,另外调用sum获取总和var adder=new LongAdder();for(...) pool.submit(()->{ while(...){ ... if(....)adder.increment(); } }); ... long total=adder.sum();以下代码可以平替var adder=new LongAccumulator(Long::sum,0)add.accumulate(value);
tips:increment方法不会返回原值,这样做会消除将求和分解到多个加数所带来的性能提升。
账户一有500,账户二有600,线程一从账户一中取600给账户二,线程二从账户二中取700给账户一,但是因为余额都不足,所以线程一和线程二都被阻塞了,造成死锁。
在同步嵌套中,obj1想要拿obj2的锁,恰好另一个类持有obj2的锁,想要去拿obj1的锁,因为彼此都不释放彼此的锁,造成死锁。
public class Dome03 implements Runnable { int flag=1; static Object o1=new Object(); static Object o2=new Object(); public static void main(String[] args) { Dome03 dome03=new Dome03(); Dome03 dome031=new Dome03(); dome03.flag=1; dome031.flag=2; Thread thread=new Thread(dome03); Thread threads=new Thread(dome031); thread.start(); threads.start(); } @Override public void run() { if (flag==1){ synchronized (o1){ System.out.println(Thread.currentThread().getName()+"获得第一把锁"); synchronized (o2){ System.out.println(Thread.currentThread().getName()+"获得第二把锁"); } } } if (flag==2){ synchronized (o2){ System.out.println(Thread.currentThread().getName()+"获得第二把锁"); synchronized (o1){ System.out.println(Thread.currentThread().getName()+"获得第一把锁"); } } } }}
java语言中没有任何东西可以避免或打破这种死锁。必须仔细设计程序,确保不会出现死锁。
使用ThreadLocal辅助类为各个线程提供各自的实例。例如simpleDateFormat类不是线程安全的。
可以这样为每个线程构造一个实例
public static final ThreadLocal dateFormat=ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd"));要访问具体的格式化方法,可以调用:String dateStamp=dateFormat.get().format(new Date());
在给定线程中首次调用get时,会调用构造器中的lambda表达式。此后,get方法会返回属于当前线程的那个实例。
生产者线程向队列插入元素,消费者线程则获取元素。使用队列,可以安全的从一个线程向另一个线程传递数据。
eg:银行转账程序,转账线程将转账指令对象插入一个队列,而不是直接访问银行对象。另一个线程从队列中取出指令完成转账。只有这个线程可以访问银行对象内部。因此不需要同步。
当试图向队列添加元素而队列满时,当试图向队列取元素而队列空时,阻塞队列将导致线程阻塞。在协调多个线程之间的合作时阻塞队列是一个很有用的工具。
工作线程可以周期性的将中间结果存储在阻塞队列中。其他工作线程移除中间结果,并进行进一步修改。
队列会自动的平衡负载、如果第一组线程运行的比第二组慢,第二组在对等待结果时,会阻塞。如果第一组快,队列会被填满,知道第二组赶上来。
方法 | 正常动作 | 特殊情况下的动作 |
---|---|---|
add | 添加一个元素 | 队列满,抛出IllegalStateException异常 |
element | 返回队头元素 | 队列空,抛出NoSuchElementException异常 |
offer | 添加一个元素并返回true | 队列满,返回false |
peek | 返回队头元素 | 队列空,返回null |
poll | 移除并返回队头元素 | 队列空,返回null |
put | 添加一个元素 | 队列满,阻塞 |
remove | 移除并返回队头元素 | 队列空,抛出NoSuchElementException异常 |
take 移除并返回队头元素 队列空,阻塞
ArrayBlockingQueue(int capacity,boolean fair):构造一个有指定的容积和公平性设置的阻塞队列。队列实现为一个循环数组。
LinkedBlockingQueue():构造一个无上限的阻塞队列或双向队列,实现为一个链表。
pubtFirst(e element)、putLast(e element):添加元素,在必要时阻塞;
takeFirst()、takeLast():移除并返回队头、队尾元素,必要时阻塞;
。。。。
有些应用使用庞大的并发散列映射,这些映射过于庞大,以至于无法使用size得到它的大小,因为这个方法只能返回int,当数字过于大的时候,是不可以的,mappingCount方法可以将大小作为long返回。
集合返回弱一致性的迭代器。这意味着迭代器不一定反映出他们构造之后的所有更改,但是,它们不会将同一个值返回两次,也不会抛出ConcurrenModificationException异常;但是对于在util包中的集合,如果集合在迭代器构造之后发生改变,集合的迭代器将会抛出这个异常。
并发散列映射可以高效的支持大量阅读器和一定数量的书写器;
ConcurrentLinkedQueue
调用compute方法可以提供一个键和一个计算新值的函数。更新整数计数器的映射:
map.compute(word,(k,v)->v==null?1:v+1);
map.merge(word,1L,(existingValue,newValue)->existingValue+newValue);
map.merge(word,1L,Long::sum);
javaAPI为并发散列映射提供了批操作,即使有其他线程在处理映射,这些操作也能安全执行。批操作会遍历映射,处理遍历过程中找到元素。
三种操作:
String result=map.search(threshole,(k,v)->v>1000?k:null);Long sum=map.reduceValues(threshold,Long::sum);
如果迭代访问集合的线程数超过更改集合的线程数,这样安排是很有用的;
通常最好使用java.util.concurrent包中定义的集合,而不是同步包装器;
Runnable封装一个异步运行的任务,可以把它想象成一个没有参数和返回值的异步方法。
Callable与Runnable类似但是有返回值。Callable接口是一个参数化的类型,只有一个方法call()
public interface Callable
V call() throws Exception;
}
类型参数是返回值类型,表示返回V对象的异步计算。
Future保存异步计算的结果。可以启动一个计算,将Future对象交给某个线程,然后忘记。
Future中的isDone():计算还在进行返回false;
void cancel(boolean f):可以取消计算
执行Callbale的一种方法是使用FutureTask,它实现了Future和Runnable接口。
执行器类有许多静态工厂方法,来构造线程池。
方法 | 描述 |
---|---|
newCachedThreadPool | 必要时创建新线程;空闲线程会保留60s |
newFixedThreadPool | 池中包含固定的线程;空闲线程会一直保留 |
newWorkStealingPool | 一种适合“fork-join”任务的线程池,其中复杂的任务会分解为简单任务,空闲线程会“密取”较简单的任务 |
newSingleThreadExecutor | 只有一个线程的池,会顺序的执行所提交的任务 |
newScheduledThreadPool | 用于调度执行的固定线程池 |
newSingleThreadScheduledExecutor | 用于调度执行的单线程池 |
为了得到最优的运行速度,并发线程数等于处理器内核数,在这种情况下,就应该使用固定线程池,即并发线程总数有一个上限;
单线程执行器对于性能分析很有帮助。如果临时用一个单线程池替换缓存或固定线程池,就能测量不适用并发的情况下应用的运行速度会慢多少。
Future submit(Cllable task);调用submit时,会得到一个Future对象,可用来得到结果或者取消任务Future<?> submit(Runnable task);可以使用这个对象来调用isDone、cancel或isCancelled。但是,get方法在完成的时候只是简答的返回nullFuture submit(Cllable task,T result);生成一个Future,它的get方法在完成的时候返回指定的result
在使用完一个线程池,调用shutdown,关闭线程池;
shutdownNow,取消所有尚未开始的任务。
使用连接池所做的工作:
invokeAny方法提交一个Callable对象集合中的所有对象,并返回某个已完成任务的结果。我们不知道返回的是哪个任务的结果,这往往是最快完成的那个任务。对于收索问题,如果我们愿意接受任何一种答案,就可以使用这个方法。
invokeAll方法提交一个Callable对象集合中的所有对象,这个方法会阻塞,直到所有任务都完成,并返回表示所有答案的一个Futrue对象列表。得到计算结果后,还可以像下面这样对结果进行处理:
List> tasks=...;List> result=executor.invokeAllz(tasks);for(Future result:results) processFurther(result.get());
在自己的程序中,应当使用执行器服务来管理线程而不要单个地启动线程。
在后台,fork-join框架使用了一种有效的只能方法来平衡可用线程的工作负载,这种方法称为工作密取。每个工作线程都有一个双端队列来完成任务。一个工作线程将子任务压入其双端队列队头。一个工作线程空闲时,他会从另一个双端队列的队尾“密取”一个任务。
如果向一个fork-join池增加很多阻塞任务,会让它无法有效工作;
到目前为止,我们的并发计算方法都是先分解一个任务,然后等待,直到所有部分都已经完成。但是等待不是个好主意。下面将解决这个问题,使得无需等待或异步计算
CompletableFuture可以采用两种方式完成:得到一个结果,或者有一个未捕获的异常。要处理这两种情况,可以使用whenComplete方法。要对结果和异常调用所提供的函数。
f.whenComplete((s,t)->{ if(t==null){process the result s;} else{process the Throwable t;} });
CompltableFuture之所以被称为时可完成的,是因为你可以手动的设置一个完成值。当,用supplyAsync创建一个CompletableFuture时,任务完成时就会隐式的设置完成值。
两个任务可以同时计算一个答案:
var f=new CompletableFuture();executor.execute(()-> { int n=workHard(arg); f.complete(n); });executor.execute(()-> { int n=workHard(arg); f.complete(n); });要对一个异常完成future,需要调用Throwable t=...;f.completeExceptionally(t);
可以在多个线程中在同一个future上安全地调用complete或completeExceptionally。如果这个future已经完成,这些调用没有任何作用。
isDone方法指出一个Future对象是否已经完成。workHard和workSmart方法可以使用这个信息停止工作。
tips:与普通地future不同,调用cancel方法时,CompletableFuture地计算不会中断。
待补充。。。
ProcessBuilder类可以取代Runtime.exec调用,而且更为灵活。