19.11.21 Java遗忘总结
- bit和byte
- 第三章 数组
- 第四章 面向对象编程(上)
- 第八章 多线程
- Java的数据类型
- this和super的作用
- 子类和父类
- final
- 对象相等
- 子类对父类的覆盖原则
- 方法重载
- 抽象类和抽象方法
- 接口
- 可变参数
- 参数绑定
- 构造器
- 方法覆盖和方法重载
- 抽象方法和抽象类
- 静态方法
- 包
- 作用域
- 反射
- 泛型
- 集合
- 并发
bit和byte
1.3 java与C++的区别
1.3 java语言为啥具有跨平台性?
1.4 Java语言运行机制及运行过程
1.5 什么是JDK, JRE
2.1 关键字与保留字
2.1.1 什么是关键字
###2.1.2 什么是保留字
2.2 标识符(Identifier)
2.3 变 量
2.3.1 什么是变量
2.3 .2 变量的分类
2.3.3 成员变量和局部变量
2.3.4 整数类型
2.3.5 浮点类型
2.3.6 字符类型
2.3.7 ASCII码
2.3.8 Unicode 编码
2.3.9 UTF-8
2.3.10 布尔类型
2.3.11 基本数据类型转换
2.3.12 字符串类型: String
2.3.13 强制类型转换
2.4 运算符
2.4.1 算术运算符
2.4.2 运算符: 赋值运算符
2.4.3 运算符: 比较运算符
2.4.4 运算符:逻辑运算符
2.4.5 运算符:位运算符
2.4.6 运算符:三元运算符
2.4.7 运算符的优先级
第三章 数组
3.1 数组概述
3.1.1 一维数组的使用
new出来的对象和数组都放在堆内存中,new出来的数组第一个元素的首地址传给栈内存的局部变量,这样使局部变量和对象联系起来。
3.3 多维数组
3.4 数组中涉及到的常见算法
3.4.1 二分法查找算法
//二分法查找:要求此数组必须是有序的。
public class Test {
public static void main(String[] args) {
int[] arr3 = new int[]{-99,-54,-2,0,2,33,43,256,999};
boolean isFlag = true;
int number = 256;
int head=0;
int tail =arr3.length-1;
int middle;
while(head<=tail) {
middle = (head + tail)/2;
if(arr3[middle]==number) {
System.out.println("找到指定的元素,索引为: " + middle);
isFlag = false;
break;
}else if (arr3[middle]>number) {
tail = middle-1;
}else {
head = middle+1;
}
if(false) {
System.out.println("找到指定的元素");
}
}
}
}
3.4.2 排序算法
3.4.2.1 选择排序
public class Sort1{
public static void main(String[] args) {
int[] nums = new int[] {2,4,1,6,4,9,41} ;
Sort1(nums);
System.out.println(Arrays.toString(nums));
}
private static void Sort1(int[] nums) {
int N =nums.length; //获取数组长度
for(int i=0;i<N-1;i++) { //从0开始 到n-1位
int min =i; //设置最小值索引为i
for(int j=i+1;j<N;j++) { //从最小索引后一位开始到数组末尾 开始遍历
if(nums[j]<nums[min]) { //只要找到比min小的索引,就使min为该索引
min = j;
}
}
int tmp = nums[i]; //交换
nums[i] = nums[min];
nums[min] = tmp;
}
}
}
3.4.2.2 冒泡排序
public class BubbleSort {
public static void main(String[] args) {
int[] nums = new int[] {2,4,1,6,4,9,41} ;
Sort1(nums);
System.out.println(Arrays.toString(nums));
}
private static void Sort1(int[] nums) {
boolean flg =false; //初始设置为false,意味着数组无序
for(int i =nums.length-1;i>0 && ! flg;i--) { //从数组末尾开始,当i>0并且数组无序时。
flg = true; //设置为标志位有序
for(int j=0;j<i;j++) {
if(nums[j+1]<nums[j]) {
int tmp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = tmp;
flg = false;
}
//只要一轮循环不发生交换,说明数组这时候是有序的,flg就是true 然后就会跳出循环
//只要一轮循环中有交换,那么flg = false 就说明数组这时候是无序的,所以继续进行循环
}
}
}
}
3.4.2.2 插入排序
public class InsertSort {
public static void main(String[] args) {
int[] nums = new int[] {2,4,1,6,4,9,41} ;
Sort(nums);
System.out.println(Arrays.toString(nums));
}
private static void Sort(int[] nums) {
for(int i=1;i<nums.length;i++) { //从下标1开始向前插入,直到最后一位
//初始化j=1,当j>0 并且 nums[j]小于它前面一位时进入循环
//从到i-1索引的数组是有序的,
for(int j =i;j>0 && nums[j-1]>nums[j];j-- ) {
int tmp = nums[j-1];
nums[j-1] = nums[j];
nums[j] = tmp;
}
}
}
}
3.4.2.3 快速排序
第四章 面向对象编程(上)
4.1 面向过程与面向对象(三大特征)
类的成员:
4.3 类的内存解析
4.4 成员变量
4.4.1 成员变量与局部变量
4.4.2 成员变量初始化赋值
4.6 方法重载
4.6 值传递机制
4.7 面向对象特征之一:封装和隐藏
4.7 四种访问权限修饰符
4.8 构造方法
4.9 关键字—this
5.1 面向对象特征之二:继承性
5-2 方法的重写
5.6 面向对象特征之三:多态性
报错,编译看左 运行看右,编译的时候是person 没有子类的earnmoney方法 所以报错
eat方法 子类父类都有,运行时看右 是子类 所以调用子类的方法
5.6 方法重写和重载的区别
5.6 子类和父类的类型转换
5.7 Object 类的使用
5.7.1 ==操作符与equals方法
println方法:
对于数组,char型的输出的是内容,其他类型的数组输出的都是地址值。
5.8 包装类(Wrapper)
6.1 关键字: static
6.1 单例设计模式
6.2 理解main方法的语法
public class OtherThing {
public static void main(String[] args) {
Something something =new Something();
something.main(null);
}
}
class Something {
public static void main(String[] something_to_do) {
System.out.println("Do something ...");
}
}
6.3 类的成员之四:代码块
6.4 关键字: final
6.6 接口
6.7 类的成员之五:内部类
第八章 多线程
8.1 基本概念:程序、进程、线程
8.1 基本概念:并行 并发
8.1 多线程优点
8.2 线程的创建和使用
8.2 Thread类的有关方法
yield是当前线程让出CPU资源,然后继续和别的线程进行竞争,谁争取到不一定,也有可能还是当前线程争取到CPU资源
sleep是让当前线程睡眠,使其丧失CPU资源,CPU运行别的线程,在睡眠时间内 线程都是阻塞状态,超过睡眠时间,线程变成可运行状态,等待CPU分配资源。
8.2 线程调度策略
public class ThreadTest1 {
public static void main(String[] args) {
MyThread1 myThread = new MyThread1();
Thread thread = new Thread(myThread);
thread.start();
}
}
class MyThread1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 50; i++) {
if(i%2==0){
System.out.println(i);
}
}
}
}
8.4 线程的同步
同步机制:1. 同步代码块 2. 同步方法
synchronized 在同步代码块的使用:
1对于继承Thread 类的方式,创建多个线程:
错误:new出多个线程 ,每个线程里都有一个obj 这样锁不唯一。如何解决?
加一个static 保证obj成员变量唯一
第二种方式: 当前对象
这样写错误 this有多个对象:
第二种:使用反射
类也是对象
- 对于实现Runnable 接口实现多线程的方式:
第一种方式:随便new一个对象就行。
因为在main方法中只会new一个实现Runnable 接口的类,这个类中只有一个obj对象 保证了多个线程锁的唯一性。
第二种方式:当前对象
this指的就是w 当前对象,也就是说调用run方法的对象,该run方法在Window1里定义的,那么这时候Window1的对象 就是this
同步方法:
修饰实现Runnable接口的
修饰继承Thread类的:
加一个static
有问题 this 是三个对象
8.4 释放锁和不释放锁的操作
8.4 单例设计模式之懒汉式(线程安全)
8.4 死锁
8.4 Lock(锁)
8.6 JDK5.0 新增线程创建方式
Java的数据类型
Java的数据类型分两种:
-
基本类型:byte,short,int,long,boolean,float,double,char
-
引用类型:所有class和interface类型
引用类型可以赋值为null,表示空,但基本类型不能赋值为null,可以用包装类型把一个基本类型视为对象(引用类型)。
this和super的作用
关键字 this 有两个用途:一是引用隐式参数,二是调用该类其他的构造器 ,
同样,super 关键字也有两个用途:一是调用超类的方法,二是调用超类的构造器。
在调用构造器的时候,调用构造器的语句只能作为另一个构造器的第一条语句出现。
def foo():
r = some_function()
if r==(-1):
return (-1)
# do something
return r
def bar():
r = foo()
if r==(-1):
print('Error')
else:
pass
一旦出错,还要一级一级上报,直到某个函数可以处理该错误(比如,给用户输出一个错误信息)。所以高级语言通常都内置了一套try…except…finally…的错误处理机制,Python也不例外。
子类和父类
- 子类不能直接访问父类private的数据域,虽然子类同样继承了一样的数据域,如需要访问,需要利用super关键词。
double basesalary=super.getSalary();
新建一个变量basesalary ,利用父类的方法得到父类的数据域 salary,然后让他们相等。
-
一个子类只能有一个父类,但是可以拥有多个接口,一个父类可以拥有多个子类。
-
public 父类的方法 子类可以调用,private父类的方法 子类不可以调用。
置换法则
is-a 规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换,例如, 可以将一个子类的对象赋给超类变量。
Employee e
e=new Employee();OK
e=new Manager();OK
Manager e
e=new Employee();error
一个 Employee 变量既可以引用一个 Employee 类对象, 也可以引用一个 Employee 类的任何一个子类的对象(例如, Manager、 Executive、 Secretary 等) .
然而,不能将一个超类的引用赋给子类变量。
final
- 不允许扩展的类被称为 final 类。如果在定义类的时候使用了 final 修饰符就表明这个类是 final 类, (final 类中的所有方法自动地成为 final 方法。)
- 类中的特定方法也可以被声明为 final。 如果这样做,子类就不能覆盖这个方法
- 域也可以被声明为 final。对于 final 域来说,构造对象之后就不允许改变它们的值了。
注意:如果将一个类声明为 final, 只有其中的方法自动地成为 final, 而不包括域。
对象相等
equals
相等的含义 : 对于实例类,例如:对于Person类,如果name相等,并且age相等,我们就认为两个Person实例相等。
equals()方法的正确编写方法:
- 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
- 用instanceof判断传入的待比较的Object是不是当前类型,如果是,继续比较,否则,返回false;
- 对引用类型用Objects.equals()比较,对基本类型直接用==比较。
使用Objects.equals()比较两个引用类型是否相等的目的是省去了判断null的麻烦。两个引用类型都是null时它们也是相等的。
对于引用字段比较,我们使用equals(),对于基本类型字段的比较,我们使用==。
public boolean equals(Object o) {
if (o instanceof Person) {
Person p = (Person) o;
return Objects.equals(this.name, p.name) && this.age == p.age;
}
return false;
}
如果不调用List的contains()、indexOf()这些方法,那么放入的元素就不需要实现equals()方法。
equals的作用及与==的区别。
equals被用来判断两个对象是否相等。
equals通常用来比较两个对象的内容是否相等,==用来比较两个对象的地址是否相等。
equals方法默认等同于“==”
Object类中的equals方法定义为判断两个对象的地址是否相等(可以理解成是否是同一个对象),地址相等则认为是对象相等。这也就意味着,我们新建的所有类如果没有复写equals方法,那么判断两个对象是否相等时就等同于“==”,也就是两个对象的地址是否相等。
但在我们的实际开发中,通常会认为两个对象的内容相等时,则两个对象相等,equals返回true。对象内容不同,则返回false。
所以可以总结为两种情况
1、类未复写equals方法,则使用equals方法比较两个对象时,相当于==比较,即两个对象的地址是否相等。地址相等,返回true,地址不相等,返回false。
2、类复写equals方法,比较两个对象时,则走复写之后的判断方式。通常,我们会将equals复写成:当两个对象内容相同时,则equals返回true,内容不同时,返回false。
hashcode( )
hashCode的作用是用来获取哈希码,也可以称作散列码。实际返回值为一个int型数据。用于确定对象在哈希表中的位置。
Object中有hashcode方法,也就意味着所有的类都有hashCode方法。
但是,hashcode只有在创建某个类的散列表的时候才有用,需要根据hashcode值确认对象在散列表中的位置。
所以,如果一个对象一定不会在散列表中使用,那么是没有必要复写hashCode方法的。但一般情况下我们还是会复写hashCode方法,因为谁能保证这个对象不会出现再hashMap等中呢?
举个例子:
两个对象equals相等的时候,hashcode并不一定相等。
1、两个对象,如果a.equals(b)==true,那么a和b是否相等?
相等,但地址不一定相等。
2、两个对象,如果hashcode一样,那么两个对象是否相等?
不一定相等,判断两个对象是否相等,需要判断equals是否为true。
Map的内部,对key做比较是通过equals()实现的。经常使用String作为key,因为String已经正确覆写了equals()方法。但如果我们放入的key是一个自己写的类,就必须保证正确覆写了equals()方法。
通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数。HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value。
正确使用Map必须保证:
- 作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()必须返回true;
- 作为key的对象还必须正确覆写hashCode()方法,且hashCode()方法要严格遵循以下规范:
如果两个对象相等,则两个对象的hashCode()必须相等;
如果两个对象不相等,则两个对象的hashCode()尽量不要相等。
子类对父类的覆盖原则
- 参数必须一样,且返回类型必须兼容,返回同样的类型或者是父类的子类。
- 不能降低方法的存取权限,比如不能将父类的public改成private 这是降低。
方法重载
方法名称相同,但是参数不同,重载与继承和多态无关。重载的方法与覆盖的方法不一样。
- 返回类型可以不同,可以任意改变。
- 不能只改变返回类型,但是参数一样
- 可以任意更改存储权限。
抽象类和抽象方法
抽象类不应该被初始化(animal)因此不能创建实例,抽象类一应要被extends,具体类应该被初始化(dog)
抽象的方法一定要被覆盖,抽象的方法没有实体
public void eat(); :没有方法体,直接分号结束。
如果声明一个抽象的方法,必须将该类也标记为抽象的,不允许在一个非抽象的类中拥有抽象的方法。这样做的好处就是多态,抽象的方法只是作为父类和子类之间的一组共同的协议。只是为了标记多态而存在,因为父类的方法不能产生对每个子类都有用的公共代码部分。具体类必须实现所有的抽象方法,以相同的方法(名称和参数)和相容的返回类型创建出非抽象的方法。
接口
接口相当于100%的纯抽象类,所有接口的方法都是抽象的,所以实现接口的具体类都要实现覆盖此方法,这样Java虚拟机在运行期间就不会搞不清楚继承哪个版本了。
不同的继承树可以实现相同的接口。
接口中绝不能含有实例域或者静态方法,但是可以包含常量。
接口不是类,尤其不能使用 new 运算符实例化一个接口,尽管不能构造接口的对象,却能声明接口的变量。接口变量必须要引用实现接口的类对象。
x=new Comparable(); error
Comparable x; ture
Comparable x=new Employee();
接口的静态变量
因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型:
public interface Person {
public static final int MALE = 1;
public static final int FEMALE = 2;
}
实际上,因为interface的字段只能是public static final类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为:
public interface Person {
// 编译器会自动加上public statc final:
int MALE = 1;
int FEMALE = 2;
}
接口的默认方法
一般来说接口的方法是抽象的,里面没有方法体,Java8对接口做了进一步的增强,在接口中可以添加使用 default 关键字修饰的非抽象方法。即:默认方法(或扩展方法),也就是说,接口的默认实现里面是有方法体的。引入默认方法的目的是为了能保证向后兼容。
在实现该接口时,该默认扩展方法在子类上可以直接使用。它的使用方式类似于抽象类中非抽象成员方法。
Note:扩展方法不能够重写(也称复写或覆盖) Object 中的方法,却可以重载Object 中的方法。
eg:toString、equals、 hashCode 不能在接口中被覆盖,却可以被重载。
如果接口中没有默认方法,那么实现该接口的所有类都需要覆盖此方法。是JAVA
无法忍受的。
默认方法允许我们在接口里添加新的方法,而不会破坏实现这个接口的已有类的兼容性,也就是说不会强迫实现接口的类实现默认方法。
默认方法和抽象方法的区别是抽象方法必须要被实现,默认方法不是。作为替代方式,接口可以提供一个默认的方法实现,所有这个接口的实现类都会通过继承得到这个方法(如果有需要也可以重写这个方法)
interface Defaulable {
//使用default关键字声明了一个默认方法
@SuppressLint("NewApi")
default String myDefalutMethod() {
return "Default implementation";
}
}
class DefaultableImpl implements Defaulable {
//DefaultableImpl实现了Defaulable接口,没有对默认方法做任何修改
}
class OverridableImpl implements Defaulable {
//OverridableImpl实现了Defaulable接口重写接口的默认实现,提供了自己的实现方法。
@Override
public String myDefalutMethod() {
return "Overridden implementation";
}
-
类中的方法优先级最高,接口中的默认方法和继承的父类方法冲突了,那么这个时候会选择父类中的方法,而不是接口中的默认方法。这个也叫做类优先原则,因此在接口中实现的一个默认方法,它不会对Java8之前写的代码产生影响。所以,我们也不能在接口中定义toString()和equals()这样的接口,因为根据类优先的原则,Object中的这些方法会保留。
-
如果无法一句第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体
-
最后,如果还是无法判断,当一个类实现两个接口的时候,其中一个接口实现了默认方法,但是另一个不管是默认方法还是抽象方法,编译器报接口冲突,让我们自己解决。继承了多个接口的类必须通过显示覆盖和调用期望的方法,显示地选择使用哪一个默认方法的实现。 如果我们要保留或者用某个接口的默认方法,只需要在覆盖方法的时候,用接口名.super.方法名,就如上面代码:
public class MyClass implements MyInterface,MyInterface2 {
@Override
public void myMethod(){
//todos...
// 也可以用某个接口的默认方法。
MyInterface2.super.myMethod();
}
可变参数
- 可变参数用类型…定义,可变参数相当于数组类型:
- 完全可以把可变参数改写为String[]类型:
public void setNames(String... names) {
this.names = names;
public void setNames(String[] names) {
this.names = names;
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入3个String
g.setNames("Xiao Ming", "Xiao Hong"); // 传入2个String
g.setNames(); // 传入0个String
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]
参数绑定
- 基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
int n = 15; // n的值为15
p.setAge(n); // 传入n的值
System.out.println(p.getAge()); // 15
n = 20; // n的值改为20
修改外部的局部变量n,不影响实例p的age字段,原因是setAge()方法获得的参数,复制了n的值,因此,p.age和局部变量n互不影响。
- 引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。
class Person {
private String[] name;
public void setName(String[] name) {
this.name = name;
String[] fullname = new String[] { "Homer", "Simpson" }; //新建了一个对象。
p.setName(fullname); // 传入fullname数组
System.out.println(p.getName()); // "Homer Simpson"
fullname[0] = "Bart"; // fullname数组的第一个元素修改为"Bart"
System.out.println(p.getName()); // "Homer Simpson"还是"Bart Simpson"?
构造器
如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
class Person {
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();,所以,Student类的构造方法实际上是这样:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
this.score = score;
}
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(); // 自动调用父类的构造方法
this.score = score;
}
}
但是,Person类并没有无参数的构造方法,因此,编译失败。解决方法是调用Person类存在的某个构造方法。例如:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}
方法覆盖和方法重载
方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。
Override(方法覆盖)和Overload(方法重载)不同的是,如果方法签名如果不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override。
方法签名:方法名和方法参数
抽象方法和抽象类
抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person类也无法被实例化。必须把Person类本身也声明为abstract,才能正确编译它。
如果一个class定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract修饰。
抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,要求其子类必须用具体的非方法覆盖其方法。
静态方法
因为静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。通过实例变量也可以调用静态方法,但这只是编译器自动帮我们把实例改写成类名而已。
包
包作用域
位于同一个包的类,可以访问包作用域的字段和方法。不用public、protected、private修饰的字段和方法就是包作用域。
一个包里可以写很多类。
在一个class中,我们总会引用其他的class。例如,小明的ming.Person类,如果要引用小军的mr.jun.Arrays类。
- 用import语句,导入小军的Arrays,然后写简单类名:
package ming;
// 导入完整类名:
import mr.jun.Arrays;
public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}
- 在写import的时候,可以使用*,表示把这个包下面的所有class都导入进来(但不包括子包的class):
// Person.java
package ming;
// 导入mr.jun包的所有class:
import mr.jun.*;
public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}
Java编译器最终编译出的.class文件只使用完整类名,因此,在代码中,当编译器遇到一个class名称时:
如果是完整类名,就直接根据完整类名查找这个class;
如果是简单类名,按下面的顺序依次查找:
-
查找当前package是否存在这个class;
-
查找import的包是否包含这个class;
-
查找java.lang包是否包含这个class。
如果按照上面的规则还无法确定类名,则编译报错。
作用域
public
- 定义为public的class、interface可以被其他任何类访问:
- 定义为public的field、method可以被其他类访问,前提是首先有访问class的权限。
private
继承有个特点,就是子类无法访问父类的private字段或者private方法,这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问:protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问
private 实例变量
- 实例域变量加了private,外部代码无法访问,但是外部代码可以调用方法间接修改private字段。 类自身的方法能够访问这些实例域, 而其他类的方法不能够读写这些域。
class Person {
private String name;
private int age;
ming.name = "Xiao Ming"; // 对字段name赋值 ERROR
ming.age = 12; // 对字段age赋值 ERROR
ming.setName("Xiao Ming"); // 设置name OK
ming.setAge(12); // 设置age OK
private 方法
定义为private的field、method无法被其他类访问。
protected
protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类:
package
包作用域是指一个类允许访问同一个package的没有public、private修饰的class,以及没有public、protected、private修饰的字段和方法。只要在同一个包,就可以访问package权限的class、field和method。
注意,包名必须完全一致,包没有父子关系,com.apache和com.apache.abc是不同的包。
反射
反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。
泛型
注意泛型的继承关系:可以把ArrayList< Integer>向上转型为List< Integer>(T不能变!类从子类ArrayList换成了父类List),但不能把ArrayList< Integer>向上转型为ArrayList< Number>(T不能变成父类:Number是Integer的父类)。
静态方法与泛型
泛型类型< T>不能用于静态方法。在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){
}
}
// 对静态方法使用<T>:
public static Pair<T> create(T first, T last) {
return new Pair<T>(first, last);
}
无法在静态方法create()的方法参数和返回类型上使用泛型类型T。
擦拭法
Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。
了解了Java泛型的实现方式——擦拭法,我们就知道了Java泛型的局限:
- < T>不能是基本类型,例如int,因为实际类型是Object,Object类型无法持有基本类型:
Pair<int> p = new Pair<>(1, 2); // compile error!
- 无法取得带泛型的Class。
Pair<String> p1 = new Pair<>("Hello", "world");
Pair<Integer> p2 = new Pair<>(123, 456);
Class c1 = p1.getClass();
Class c2 = p2.getClass();
System.out.println(c1==c2); // true
System.out.println(c1==Pair.class); // true
因为T是Object,我们对Pair< String>和Pair< Integer>类型获取Class时,获取到的是同一个Class,也就是Pair类的Class。
换句话说,所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair< Object>。
3. 无法判断带泛型的Class:
Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>.class) {
}
原因和前面一样,并不存在Pair< String>.class,而是只有唯一的Pair.class。
- 不能实例化T类型
public class Pair<T> {
private T first;
private T last;
public Pair() {
// Compile error:
first = new T();
last = new T();
}
}
泛型继承
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
一个类可以继承自一个泛型类。例如:父类的类型是Pair< Integer>,子类的类型是IntPair,可以这么继承:
public class IntPair extends Pair<Integer> {
}
无法获取Pair< T>的T类型,即给定一个变量Pair< Integer> p,无法从p中获取到Integer类型。
但是,在父类是泛型类型的情况下,编译器就必须把类型T(对IntPair来说,也就是Integer类型)保存到子类的class文件中,不然编译器就不知道IntPair只能存取Integer这种类型。
在继承了泛型类型的情况下,子类可以获取父类的泛型类型。例如:IntPair可以获取到父类的泛型类型Integer
泛型类
泛型接口
当实现泛型接口的类,未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。
即:class FruitGenerator< T> implements Generator< T>{ 两个< T> 不能少
如果不声明泛型,如:class FruitGenerator implements Generator< T>,编译器会报错:“Unknown class”
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
当实现泛型接口的类,传入泛型实参时:
FruitGenerator 后面可以没有< String>
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
泛型方法
public class GenericTest {
//这个类是个泛型类,在上面已经介绍过
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
/**
* 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
public E setKey(E key){
this.key = keu
}
*/
}
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
public <T> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
public <T> T showKeyName(Generic<E> container){
...
}
*/
/**
* 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
* 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
* 所以这也不是一个正确的泛型方法声明。
public void showkey(T genericObj){
}
*/
public static void main(String[] args) {
}
}
extends通配符
对于只读不写的程序在方法参数里使用<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。
除了可以传入Pair< Integer>类型,我们还可以传入Pair< Double>类型,Pair< BigDecimal>类型等等,因为Double和BigDecimal都是Number的子类。
static int add(Pair<Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
int sum = PairHelper.add(new Pair<Number>(1, 2)); //OK
Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);// error 方法参数类型定死了只能传入Pair<Number>。不能传入Number的子类!!
// 使用Pair<? extends Number>使得方法接收所有泛型类型为Number或Number子类的Pair类型
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);// error 方法参数类型定死了只能传入Pair<Number>。
注意程序的签名 <? extends Number>,例如
<? extends Number> getFirst();
即返回值是Number或Number的子类,因此,可以安全赋值给Number类型的变量。但是getFirst()的参数方法没有改变。
对于setFirst(T first) 方法。
使用类似<? extends Number>通配符作为方法参数时表示:
-
方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();;
-
方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);。
super通配符
Pair<? super Integer>表示,方法参数接受所有泛型类型为Integer或Integer父类的Pair类型。
void setFirst(? super Integer);
? super Integer getFirst();
Integer x = p.getFirst(); error
Object obj = p.getFirst(); OK
因为如果传入的实际类型是Pair< Number>,编译器无法将Number类型转型为Integer。
注意:虽然Number是一个抽象类,我们无法直接实例化它。但是,即便Number不是抽象类,这里仍然无法通过编译。此外,传入Pair< Object>类型时,编译器也无法将Object类型转型为Integer。
唯一可以接收getFirst()方法返回值的是Object类型。
因此
- 允许调用set(? super Integer)方法传入Integer的引用;
- 不允许调用get()方法获得Integer的引用。
对比extends和super通配符
- <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
- <?super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。
一个是允许读不允许写,另一个是允许写不允许读。
何时使用extends,何时使用super?为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super。即:如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符。
集合
List 接口
考察List< E>接口,可以看到几个主要的接口方法:
- 在末尾添加一个元素:void add(E e)
- 在指定索引添加一个元素:void add(int index, E e)
- 删除指定索引的元素:int remove(int index)
- 删除某个元素:int remove(Object e)
- 获取指定索引的元素:E get(int index)
- 获取链表大小(包含元素的个数):int size()
List接口通过两种方式实现,一是通过数组列表(即ArrayList的实现方式)来实现,二是通过链表(LinkedList)实现。
链表
每个链表都包括一个LinikedList对象和许多Node对象,LinkedList对象通常包含头和尾节点的引用,分别指向链表的第一个节点和最后一个节点。而每个节点对象通常包含数据部分data,以及对上一个节点的引用prev和下一个节点的引用next,只有下一个节点的引用称为单向链表,两个都有的称为双向链表。next值为null则说明是链表的结尾,如果想找到某个节点,我们必须从第一个节点开始遍历,不断通过next找到下一个节点,直到找到所需要的。
单向链表
一个单链表的节点(Node)分为两个部分,第一个部分(data)保存或者显示关于节点的信息,另一个部分存储下一个节点的地址。最后一个节点存储地址的部分指向空值。
单向链表只可向一个方向遍历,一般查找一个节点的时候需要从第一个节点开始每次访问下一个节点,一直访问到需要的位置。而插入一个节点,对于单向链表,我们只提供在链表头插入,只需要将当前插入的节点设置为头节点,next指向原头节点即可。删除一个节点,我们将该节点的上一个节点的next指向该节点的下一个节点。
package com.ys.datastructure;
public class SingleLinkedList {
private int size;//链表节点的个数
private Node head;//头节点
public SingleLinkedList(){
size = 0;
head = null;
}
//链表的每个节点类
private class Node{
private Object data;//每个节点的数据
private Node next;//每个节点指向下一个节点的连接
public Node(Object data){
this.data = data;
}
}
//在链表头添加元素
public Object addHead(Object obj){
Node newHead = new Node(obj);
if(size == 0){
head = newHead;
}else{
newHead.next = head;
head = newHead;
}
size++;
return obj;
}
//在链表头删除元素
public Object deleteHead(){
Object obj = head.data;
head = head.next;
size--;
return obj;
}
//查找指定元素,找到了返回节点Node,找不到返回null
public Node find(Object obj){
Node current = head;
int tempSize = size;
while(tempSize > 0){
if(obj.equals(current.data)){
return current;
}else{
current = current.next;
}
tempSize--;
}
return null;
}
//删除指定的元素,删除成功返回true
public boolean delete(Object value){
if(size == 0){
return false;
}
Node current = head;
Node previous = head;
while(current.data != value){
if(current.next == null){
return false;
}else{
previous = current;
current = current.next;
}
}
//如果删除的节点是第一个节点
if(current == head){
head = current.next;
size--;
}else{//删除的节点不是第一个节点
previous.next = current.next;
size--;
}
return true;
}
//判断链表是否为空
public boolean isEmpty(){
return (size == 0);
}
//显示节点信息
public void display(){
if(size >0){
Node node = head;
int tempSize = size;
if(tempSize == 1){//当前链表只有一个节点
System.out.println("["+node.data+"]");
return;
}
while(tempSize>0){
if(node.equals(head)){
System.out.print("["+node.data+"->");
}else if(node.next == null){
System.out.print(node.data+"]");
}else{
System.out.print(node.data+"->");
}
node = node.next;
tempSize--;
}
System.out.println();
}else{//如果链表一个节点都没有,直接打印[]
System.out.println("[]");
}
}
}
@Test
2 public void testSingleLinkedList(){
3 SingleLinkedList singleList = new SingleLinkedList();
4 singleList.addHead("A");
5 singleList.addHead("B");
6 singleList.addHead("C");
7 singleList.addHead("D");
8 //打印当前链表信息
9 singleList.display();
10 //删除C
11 singleList.delete("C");
12 singleList.display();
13 //查找B
14 System.out.println(singleList.find("B"));
15 }
双端链表
对于单项链表,我们如果想在尾部添加一个节点,那么必须从头部一直遍历到尾部,找到尾节点,然后在尾节点后面插入一个节点。这样操作很麻烦,如果我们在设计链表的时候多个对尾节点的引用,那么会简单很多。(比单向链表就多了一个尾节点)
package com.ys.link;
public class DoublePointLinkedList {
private Node head;//头节点
private Node tail;//尾节点
private int size;//节点的个数
private class Node{
private Object data;
private Node next;
public Node(Object data){
this.data = data;
}
}
public DoublePointLinkedList(){
size = 0;
head = null;
tail = null;
}
//链表头新增节点
public void addHead(Object data){
Node node = new Node(data);
if(size == 0){//如果链表为空,那么头节点和尾节点都是该新增节点
head = node;
tail = node;
size++;
}else{
node.next = head;
head = node;
size++;
}
}
//链表尾新增节点
public void addTail(Object data){
Node node = new Node(data);
if(size == 0){//如果链表为空,那么头节点和尾节点都是该新增节点
head = node;
tail = node;
size++;
}else{
tail.next = node;
tail = node;
size++;
}
}
//删除头部节点,成功返回true,失败返回false
public boolean deleteHead(){
if(size == 0){//当前链表节点数为0
return false;
}
if(head.next == null){//当前链表节点数为1
head = null;
tail = null;
}else{
head = head.next;
}
size--;
return true;
}
//判断是否为空
public boolean isEmpty(){
return (size ==0);
}
//获得链表的节点个数
public int getSize(){
return size;
}
//显示节点信息
public void display(){
if(size >0){
Node node = head;
int tempSize = size;
if(tempSize == 1){//当前链表只有一个节点
System.out.println("["+node.data+"]");
return;
}
while(tempSize>0){
if(node.equals(head)){
System.out.print("["+node.data+"->");
}else if(node.next == null){
System.out.print(node.data+"]");
}else{
System.out.print(node.data+"->");
}
node = node.next;
tempSize--;
}
System.out.println();
}else{//如果链表一个节点都没有,直接打印[]
System.out.println("[]");
}
}
}
双向链表
我们知道单向链表只能从一个方向遍历,那么双向链表它可以从两个方向遍历。
package com.ys.datastructure;
public class TwoWayLinkedList {
private Node head;//表示链表头
private Node tail;//表示链表尾
private int size;//表示链表的节点个数
private class Node{
private Object data;
private Node next;
private Node prev;
public Node(Object data){
this.data = data;
}
}
public TwoWayLinkedList(){
size = 0;
head = null;
tail = null;
}
//在链表头增加节点
public void addHead(Object value){
Node newNode = new Node(value);
if(size == 0){
head = newNode;
tail = newNode;
size++;
}else{
head.prev = newNode;
newNode.next = head;
head = newNode;
size++;
}
}
//在链表尾增加节点
public void addTail(Object value){
Node newNode = new Node(value);
if(size == 0){
head = newNode;
tail = newNode;
size++;
}else{
newNode.prev = tail;
tail.next = newNode;
tail = newNode;
size++;
}
}
//删除链表头
public Node deleteHead(){
Node temp = head;
if(size != 0){
head = head.next;
head.prev = null;
size--;
}
return temp;
}
//删除链表尾
public Node deleteTail(){
Node temp = tail;
if(size != 0){
tail = tail.prev;
tail.next = null;
size--;
}
return temp;
}
//获得链表的节点个数
public int getSize(){
return size;
}
//判断链表是否为空
public boolean isEmpty(){
return (size == 0);
}
//显示节点信息
public void display(){
if(size >0){
Node node = head;
int tempSize = size;
if(tempSize == 1){//当前链表只有一个节点
System.out.println("["+node.data+"]");
return;
}
while(tempSize>0){
if(node.equals(head)){
System.out.print("["+node.data+"->");
}else if(node.next == null){
System.out.print(node.data+"]");
}else{
System.out.print(node.data+"->");
}
node = node.next;
tempSize--;
}
System.out.println();
}else{//如果链表一个节点都没有,直接打印[]
System.out.println("[]");
}
}
}
增加节点
- 将新节点的next指向头/尾节点 node.next=head;
- 将新节点赋给头结点或者尾节点 head=node;
- 链表长度加1; size++;
删除节点
- 将要删除的节点的data指给新的一个变量 Object obj = head.data;
- 将删除的下一节点赋给头结点,或者将删除的前一节点赋给尾节点。 head = head.next;
- 链表长度减1; size–;
Map 接口
最常用的实现类是HashMap,其是无序的。
put(K key, V value) // 把key和value做了映射并放入Map
V get(K key)//可以通过key获取到对应的value。如果key不存在,则返回null。
boolean containsKey(K key) //查询某个key是否存在
始终牢记:Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。
Map存储的是key-value的映射关系,并且,它不保证顺序。在遍历的时候,遍历的顺序既不一定是put()时放入的key的顺序,也不一定是key的排序顺序。以HashMap为例,假设我们放入"A",“B”,"C"这3个key,遍历的时候,每个key会保证被遍历一次且仅遍历一次,但顺序完全没有保证。
HashMap
HashMap 是嘻哈映射,TreeMap是实现了树形结构。
教程1 关于方法介绍详细。
教程2 关于扩容介绍详细
教程3
TreeMap
SortedMap是接口,它的实现类是TreeMap。
使用TreeMap时,如果不指定自定义的比较器Comparator,那么插入的对象必须实现Comparable接口。元素按照实现此接口的compareTo()方法去排序,String、Integer这些类已经实现了Comparable接口,因此可以直接作为Key使用。如果指定了自定义的比较器Comparator,优先使用Comparator去对元素进行排序(可自定义排序规则)。比较规则决定了元素是否可以重复,以及元素的排序结果。
TreeMap不使用equals()和hashCode()。
TreeSet底层是二叉树实现的,当存储元素的时候,会调用比较规则方法和二叉树上的元素一一比较,如果要插入的元素比当前元素小就到左子树去比较,如果比当前元素大,就到右子树去比较,直到当前元素的左或者右子树为空,就插入此元素。如果在比较过程中,出现当前元素等于要插入的元素,那么此元素不插入,例如上例中最后一个元素3被过滤掉了,这样也就保证了Set的元素唯一性。
树的基础知识
1. 二叉查找树
特殊的二叉树,又称为排序二叉树、二叉搜索树、二叉排序树。
二叉查找树实际上是数据域有序的二叉树,即对树上的每个结点,都满足其左子树上所有结点的数据域均小于或等于根结点的数据域,右子树上所有结点的数据域均大于根结点的数据域。如下图所示:
这样在查找的时候就不用遍历整个树而且可以做到有方向的查找,效率较高。
2. 平衡二叉树
也称作AVL树,AVL树本质还是一棵二叉查找树,只是在其基础上增加了“平衡”的要求。所谓平衡是指,对AVL树的任意结点来说,其左子树与右子树的高度之差的绝对值不超过1,其中左子树与右子树的高度因子之差称为平衡因子。()
如果在AVL树中进行插入或删除节点,可能导致AVL树失去平衡,这种失去平衡的二叉树可以概括为四种姿态:LL(左左)、RR(右右)、LR(左右)、RL(右左)。它们的示意图如下:
这四种失去平衡的姿态都有各自的定义:
LL:LeftLeft,也称“左左”。插入或删除一个节点后,根节点的左孩子(Left Child)的左孩子(Left Child)还有非空节点,导致根节点的左子树高度比右子树高度高2,AVL树失去平衡。
RR:RightRight,也称“右右”。插入或删除一个节点后,根节点的右孩子(Right Child)的右孩子(Right Child)还有非空节点,导致根节点的右子树高度比左子树高度高2,AVL树失去平衡。
LR:LeftRight,也称“左右”。插入或删除一个节点后,根节点的左孩子(Left Child)的右孩子(Right Child)还有非空节点,导致根节点的左子树高度比右子树高度高2,AVL树失去平衡。
RL:RightLeft,也称“右左”。插入或删除一个节点后,根节点的右孩子(Right Child)的左孩子(Left Child)还有非空节点,导致根节点的右子树高度比左子树高度高2,AVL树失去平衡。
四种情况的解决方式与实现
教程1
3. 2-3 树
2-3树是平衡的3路查找树,其中2(2-node)是指拥有两个分支的节点,3(3-node)是指拥有三个分支的节点。
对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key小,有节点也是一个2-3节点,所有的值比key要大。
对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的key还要小;中间节点也是一个2-3节点,中间节点的key值在两个根节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个key中的最大的key还要大。
4. 红黑树
并发
线程
教程1 讲创建线程,线程运行结果与执行顺序无关,线程实例变量与安全问题,停止线程,线程优先级,守护线程,线程让步。
线程共包括一下5种状态:
-
新建、初始状态(New) :线程对象被创建后就进入了新建状态,Thread thread = new Thread();
-
就绪(Runnable):也被称之为“可执行状态”,当线程被new出来后,其他的线程调用了该对象的start()方法,即thread.start(),此时线程位于“可运行线程池”中,只等待获取CPU的使用权,随时可以被CPU调用。进入就绪状态的进程除CPU之外,其他运行所需的资源都已经全部获得。
-
运行(Running):线程获取CPU权限开始执行。注意:线程只能从就绪状态进入到运行状态。
-
阻塞(Bloacked):阻塞状态是线程因为某种原因放弃CPU的使用权,暂时停止运行,直到线程进入就绪状态后才能有机会转到运行状态。
阻塞的情况分三种:
等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池中”。进入这个状态后是不能自动唤醒的,必须依靠其他线程调用notify()或者notifyAll()方法才能被唤醒。
同步阻塞:运行的线程在获取对象的(synchronized)同步锁时,若该同步锁被其他线程占用,则JVM会把该线程放入“锁池”中。
其他阻塞:通过调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新回到就绪状态
以上三种阻塞状态请参考上面的2个图示来理解。
- 死亡(Dead):线程执行完了或因异常退出了run()方法,则该线程结束生命周期。
继承Thread类和实现Runnable接口的区别
继承Thread:线程代码存放在Thread子类run方法中。
优势:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。
劣势:已经继承了Thread类,无法再继承其他类。
实现Runnable:线程代码存放在接口的子类的run方法中。
优势:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
劣势:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。
线程让步—yield:
该方法和sleep方法类似,也是Thread类提供的一个静态方法,可以让正在执行的线程暂停,但是不会进入阻塞状态,而是直接进入就绪状态。相当于只是将当前线程暂停一下,然后重新进入就绪的线程池中,让线程调度器重新调度一次。也会出现某个线程调用yield方法后暂停,但之后调度器又将其调度出来重新进入到运行状态。
public class SynTest {
public static void main(String[] args) {
yieldDemo ms = new yieldDemo();
Thread t1 = new Thread(ms,"张三吃完还剩");
Thread t2 = new Thread(ms,"李四吃完还剩");
Thread t3 = new Thread(ms,"王五吃完还剩");
t1.start();
t2.start();
t3.start();
}
}
class yieldDemo implements Runnable{
int count = 20;
public synchronized void run() {
while (true) {
if(count>0){
System.out.println(Thread.currentThread().getName() + count-- + "个瓜");
if(count % 2 == 0){
Thread.yield(); //线程让步
}
}
}
}
}
package com.yield;
public class YieldTest extends Thread {
public YieldTest(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 50; i++) {
System.out.println("" + this.getName() + "-----" + i);
// 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
if (i == 30) {
this.yield();
}
}
}
public static void main(String[] args) {
YieldTest yt1 = new YieldTest("张三");
YieldTest yt2 = new YieldTest("李四");
yt1.start();
yt2.start();
}
}
sleep和yield的区别:
① sleep方法声明抛出InterruptedException,调用该方法需要捕获该异常。yield没有声明异常,也无需捕获。
② sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态。
wait(), notify(), notifyAll()方法
1.wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)
2.notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
3.wait(long timeout)让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
1、synchronized(t1)锁定t1(获得t1的监视器)
2、synchronized(t1)这里的锁定了t1,那么wait需用t1.wait()(释放掉t1)
3、因为wait需释放锁,所以必须在synchronized中使用(没有锁定则么可以释放?没有锁时使用会抛出IllegalMonitorStateException(正在等待的对象没有锁))
-
notify也要在synchronized使用,应该指定对象,t1. notify(),通知t1对象的等待池里的线程使一个线程进入锁定池,然后与锁定池中的线程争夺锁。那么为什么要在synchronized使用呢? t1. notify()需要通知一个等待池中的线程,那么这时我们必须得获得t1的监视器(需要使用synchronized),才能对其操作,t1. notify()程序只是知道要对t1操作,但是是否可以操作与是否可以获得t1锁关联的监视器有关。
-
synchronized(),wait,notify() 对象一致性
-
在while循环里而不是if语句下使用wait(防止虚假唤醒spurious wakeup)
class ThreadA extends Thread{
public ThreadA(String name) {
super(name);
}
public void run() {
synchronized (this) {
try {
Thread.sleep(1000); // 使当前线阻塞 1 s,确保主程序的 t1.wait(); 执行之后再执行 notify()
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" call notify()");
// 唤醒当前的wait线程
this.notify();
}
}
}
public class WaitTest {
public static void main(String[] args) {
ThreadA t1 = new ThreadA("t1");
synchronized(t1) {
try {
// 启动“线程t1”
System.out.println(Thread.currentThread().getName()+" start t1");
t1.start();
// 主线程等待t1通过notify()唤醒。
System.out.println(Thread.currentThread().getName()+" wait()");
t1.wait(); // 不是使t1线程等待,而是当前执行wait的线程等待
System.out.println(Thread.currentThread().getName()+" continue");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.b510.test;
5
6 /**
7 * java中的sleep()和wait()的区别
8 * @author Hongten
9 * @date 2013-12-10
10 */
11 public class TestD {
12
13 public static void main(String[] args) {
14 new Thread(new Thread1()).start();
15 try {
16 Thread.sleep(5000);
17 } catch (Exception e) {
18 e.printStackTrace();
19 }
20 new Thread(new Thread2()).start();
21 }
22
23 private static class Thread1 implements Runnable{
24 @Override
25 public void run(){
26 synchronized (TestD.class) {
27 System.out.println("enter thread1...");
28 System.out.println("thread1 is waiting...");
29 try {
30 //调用wait()方法,线程会放弃对象锁,进入等待此对象的等待锁定池
31 TestD.class.wait();
32 } catch (Exception e) {
33 e.printStackTrace();
34 }
35 System.out.println("thread1 is going on ....");
36 System.out.println("thread1 is over!!!");
37 }
38 }
39 }
40
41 private static class Thread2 implements Runnable{
42 @Override
43 public void run(){
44 synchronized (TestD.class) {
45 System.out.println("enter thread2....");
46 System.out.println("thread2 is sleep....");
47 //只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
48 TestD.class.notify();
49 //==================
50 //区别
51 //如果我们把代码:TestD.class.notify();给注释掉,即TestD.class调用了wait()方法,但是没有调用notify()
52
enter thread1...
thread1 is waiting...
enter thread2....
thread2 is sleep....
thread2 is going on....
thread2 is over!!!
thread1 is going on ....
thread1 is over!!!
如果注释掉代码:
1 TestD.class.notify();
enter thread1...
thread1 is waiting...
enter thread2....
thread2 is sleep....
thread2 is going on....
thread2 is over!!!
且程序一直处于挂起状态。
//方法,则线程永远处于挂起状态。
53 try {
54 //sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,
55 //但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
56 //在调用sleep()方法的过程中,线程不会释放对象锁。
57 Thread.sleep(5000);
58 } catch (Exception e) {
59 e.printStackTrace();
60 }
61 System.out.println("thread2 is going on....");
62 System.out.println("thread2 is over!!!");
63 }
64 }
65 }
66 }
sleep() 方法
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复可运行状态。
可运行状态,不是运行状态。sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始执行。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
**wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 。**注意:wiat()必须放在synchronized block中,否则会在program runtime时扔出“java.lang.IllegalMonitorStateException”异常。
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
同步
线程运行的两大必要资源:对象的锁,和CPU资源。,都拿到的时候,线程才进入运行状态。
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突。
同步方法
同步方法有三种:
同步方法1 synchronized关键字修饰方法 教程1
即有synchronized关键字修饰的方法。由于java的每个对象都有一个对象锁,当用此关键字修饰方法时, 对象锁会保护整个方法。在调用该方法前,需要获得对象锁,否则就处于阻塞状态。
synchronized 修饰方法时锁定的是调用该方法的对象。它并不能使调用该方法的多个对象在执行顺序上互斥。 例子,重要!
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
同步方法2 同步代码块
被synchronized该关键字修饰的语句块会自动被加上对象锁,从而实现同步
synchronized(this){
count +=money;
}
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
同步方法3 使用特殊域变量(volatile)实现线程同步
a.volatile关键字为成员变量变量的访问提供了一种免锁机制,
b.使用volatile修饰成员变量相当于告诉虚拟机该域可能会被其他线程更新,
c.因此每次使用该成员变量就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
volatile不能保证原子操作,因此volatile不能代替synchronized。此外volatile会组织编译器对代码优化,因此能不使用它就不使用它吧。它的原理是每次要线程要访问volatile修饰的变量时都是从内存中读取,而不是存缓存当中读取,因此每个线程访问到的变量值都是一样的。这样就保证了同步。
教程1,很棒!
教程2
监视器和锁
锁和监视器之间的关系: 锁为实现监视器提供必要的支持。
**逻辑上锁是对象内存堆中头部的一部分数据。**JVM中的每个对象都有一个锁(或互斥锁),任何程序都可以使用它来协调对对象的多线程访问。如果任何线程想要访问该对象的实例变量,那么线程必须拥有该对象的锁(在锁内存区域设置一些标志)。所有其他的线程试图访问该对象的变量必须等到拥有该对象的锁有的线程释放锁(改变标记)。
一旦线程拥有一个锁,它可以多次请求相同的锁,但是在其他线程能够使用这个对象之前必须释放相同数量的锁。如果一个线程请求一个对象的锁三次,如果别的线程想拥有该对象的锁,那么之前线程需要 “释放”三次锁。
显示锁用lock() 和unlock()语法。
…
private Lock bankLock = new ReentrantLock();
…
public double getTotalBalance()
{
bankLock.lock();
try
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
finally
{
bankLock.unlock();
}
}
1) 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
2) 锁可以管理试图进入被保护代码的线程
3) 锁可以拥有一个或者多个相关的条件对象
4) 每个条件对象管理那些已经进入被保护的代码段,但还不能运行的线程
Lock和Condition接口为程序设计人员提供了高度的锁定控制。然后大多数情况下,并不需要这样的控制,用内部锁
用synchronized关键字来编写代码简洁得多。当然要理解这一代码,你必须了解每一个对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程。
实际上推荐最好优先使用BlockQueue,Excutor,同步集合等,然后再是synchronized关键字,最才是Lock/Condition
监视器是一中同步结构,它允许线程同时互斥(使用锁)和协作,即使用等待集(wait-set)使线程等待某些条件为真的能力。JVM为每一个对象和类都关联一个锁(内置锁),锁住了一个对象,就是获得对象相关联的监视器”
锁为监视器的实现提供了必要的支持。
监视器解释1
监视器解释2