1.JVM
内存划分: 寄存器:给CPU使用,和开发无关; 本地方法栈:JVM在使用操作系统功能的时候使用,和开发无关。 方法区:存储可以运行的class文件 堆内存:存储对象或者数组,new来创建的,都存储在堆内存。 方法栈:方法运行时使用的内存,比如main方法运行,进入方法栈中执行。
2.数组
数组的概念:数组就是存储数据长度固定的容器,保证多个数据类型要一致。数组有定长特定,长度一旦指定,不可更改。 数组的索引:每个存储到数组的元素都会拥有一个编号,从0开始,这个自动编号称为数组索引可以根据索引访问到数组的元素。 为元素赋值:数组名[索引]=数值; 获取数组中的元素:变量名=数组名[索引] 数组原理内存图: java虚拟机要运行程序必须对内存进行分配和管理,为了提高运算效率对空间进行了不同区域的划分, jvm内存划分: 寄存器:给CPU使用,和开发无关; 本地方法栈:JVM在使用操作系统功能的时候使用,和开发无关。 方法区:存储可以运行的class文件 堆内存:存储对象或者数组,new来创建的,都存储在对内存。 方法栈:方法运行时使用的内存,比如main方法运行,进入方法栈中执行。
数组变量存的是数组在对内存中的地址, 两个变量指向同一个数组,都可以对数组操作,改变的是堆内存的数组。 数组=null 意味着将不会保存数组的内存地址,也就不允许再操作数组了。 数组的遍历:定义变量,保存数组0引用的元素,遍历数组,将遍历到的数组元素和变量比较,如果数组元素比变量值大,就将数组元素赋值给变量,最终获得的变量值就是最大的数组元素。 方法的参数为基本类型时传递的是数据值,方法的参数为引用类型时传递的时地址值。
抽象类:abstract class
父类设计的非常抽象,没有具体的实例,不需要创建该类的实例时可以用抽象类;让其子类实现这个类的抽象方法。 抽象类的特征:1,不可被实例化2,抽象类是有构造器的(所有类都有构造器)3,抽象方法所在的类一定是抽象类4,抽象类可以没有抽象方法 抽象方法的特征:1,抽象方法没有方法体,需要继承该抽象类的子类来重写该抽象方法。2如果子类继承该抽象类,并重写了父类的所有抽象方法,此类不是抽象类可以实例化。 3如果子类继承抽象类,没有重写所有的抽象方法,意味着子类中还有抽象方法,此子类要声明为抽象类。 抽象类不能用final修饰,定义抽象类目的就是让其他类继承的。 接口中不能有main方法和构造器。
2.static
static修饰的成员变量和成员方法,都属于类的,可以不靠创建对象来调用。 类变量:被static修饰的成员变量,该类任何对象都可以修改,也可以在不创建对象的情况下对类变量进行操作。 类方法:被static修饰的成员方法习惯称为静态方法。 静态方法可以直接访问类变量和静态方法; 静态方法不能直接访问普通成员变量或成员方法,反之,成员方法可以直接访问类变量或静态方法。 静态方法中不能使用this关键字。总之静态方法只能访问静态成员。 被static修饰的成员可以并且建议通过类名直接访问,虽然也可以通过对象名访问静态成员,但是会出现警告。 static修饰的内容是随着类的加载而加载的,且只加载一次,存储于一块固定的内存区域(方法区中的静态区),所以可以直接用类名调用。他优先于对象存在,所以可以被所有对象共享。 静态代码块:定义在成员位置,用static修饰的代码{};类中方法外,随着类的加载而执行且执行一次,优先于main方法和构造方法的执行。作用:给类变量进行初始化赋值。 static可以修饰变量,方法,代码块,主要目的是再不创建对象的情况下去调用方法,例如Arrays和Math. Arrays此类包含操作数组的各种方法,比如排序和搜索,其所有方法均为静态方法。 Math包含用于执行基本数学运算的方法,如指数、对数、平方根、三角函数。其所有方法也是静态方法。
二、Java进阶
1. == 和equals方法
Equals:只能比较引用类型的数据(重写前比较的是堆内存中的地址,重写后一般比较的是对象的属性)
默认地址比较:如果没有覆盖重写equals方法,那么object类中默认进行==运算符的对象地址比较,只要不是 同一个对象,结果必然时false。
对象内容比较:如果希望对象的内容比较,即所有或指定的部分成员变量相同就判定两个对象相同,则可以覆 盖重写equals方法。
String、Integer、Date在这些类中equals有其自身的实现,比较的时内容一致,而不是比较类在堆内存中存放 的地址了。
==:基本类型比较的是值,引用类型比较的是地址。
对于引用类型而言,在没有重写Object类的equals方法时两者比较的结果一样
2. Objects类
在JDK1.7添加了一个Objects工具类,它提供了一些方法来操作对象,它由一些静态的实用方法组成,这些方法 时null-save(空指针安全的)或null-tolearnt(容忍空指针的),用于计算对象的hashcode、返回对象的字符串表示 形式、比较两个对象。
在比较两个对象的时候,Object的equals方法容易抛出空指针异常,而objects类中的equals方法则优化了这个 问题。
3. 日期时间类
-
Date类:表示特定的瞬间,精确到毫秒。
Public Date():分配Date对象并初始化此对象,以表示分配它的时间(精确到毫秒)。
Public Date(long date): 分配Date对象并初始化此对象,表示自基准之间以来的指定的毫秒数。
在使用println的时候会自动调用Date类中的toString方法。Date类对Object类中的toString方法进行了覆盖重写,所以结果为指定格式的字符串。
常用方法:public long getTime(),把日期对象转换成对应的时间毫秒值。
-
DateFormat类
Java.text.DateFormat是日期/时间格式化子类的抽象类,可以将Date对象与String对象之间进行来回转换。
格式化:按指定的格式从Date对象转换为String对象
解析:按照执行格式从String对象转为Date对象。
DateFormat是抽象类不能直接使用,常用的子类java.test.SimpleDateFormat.
常用的方法:format;parse
-
Calendar类
Java.util.Calendar是日历类,在Date后出现,替换掉了许多Date的方法。时间信息都封装成了静态变量。
Calendar为抽象类,通过静态方法创建返回子类对象。
静态方法:Calendar.getInstanse();
常用方法:public int get(int field):返回指定日历字段的值。
Public void set(int field,int value):将给定的日历字段设置为指定的值;
Public abstract void add(int field,int amount):根据日历的规则,为指定的日历字段添加或减去指定的时间量。
Public Date getTime():返回一个表示此Calendar时间值(从历元到现在的毫秒偏移量)的Date对象。
成员变量:YEAR 年、MONTH 月(从0开始,可以+1使用)、DAY_OF_MONTH 月中的天(几号)、HOUR 小时(12小时制)、HOUR_OF_DAY(24小时制)、MINUTE 分、SECOND秒、DAY_OF_WORK 周中的天(周日为1,可以减1使用)
常用方法:get(?)/set(?,?)、add(?,?)、getTime()
4. System类
Java.long.System类中提供了大量的静态方法,常用的有:
Public static long currentTimeMillis():返回以秒为单位的当前时间。
Public static void arraycopy(Object src,int srcpos,Object dest,int destpos,int length):将数组中指定的数据拷 贝到另一个数组中。
CurrentTimeMillis方法就是获取当前系统时间与1970-01-01 00:00点之间的毫秒差值,可以用来统计一段代 码运行的毫秒时间。
Arraycopy方法数组的拷贝动作是系统及的性能很高,System.arraycopy方法具有5个参数,含义分别是:
序号 参数名 参数类型 参数含义
1 src Object 源数组
2 srcPos int 源数组索引起始位置
3 dest Object 目标数组
4 destPos int 目标数组索引起始位置
5 length int 复制元素个数
5. String,StringBuffer,StringBuilder三者的区别
三者都是final类,不允许被继承,主要区别在两个方面,运行速度和线程安全方面。
运行速度:StringBuillder>StringBuffer>String.
String是字符串常量,而StringBuffer和StringBuilder均为字符串变量,即String对象一旦创建是不可更改的, 但后者是可以更改的。
Java中对String的操作实际上是不断创建新对象并将就对象回收的过程,所以执行速度慢。
StringBuffer和StringBuilder可以通过append()操作对象。
StringBuffer是线程安全的,StringBuilder是线程不安全的;
StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法带有synchronized关键字,可以保 证线程安全。
6. 包装类
基本类型效率高,引用类型功能多,可以使用基本类型的包装类运用更多功能。
-int Integer char Character
装箱:从基本类型转换为对应的包装类对象—使用构造函数或者包装类中的valueOf()方法
拆箱:从包装类对象转换为基本类型
7. Collection集合
集合是java中提供的一种容器,可以用来存储多个数据。
数组的长度是固定的,集合的长度是可变的。
数组中存储的是同一类型的元素,可以存储基本类型数值,集合存储的都是对象,对象的类型可以不一致。
单例集合:java.util.Collections;双列集合:java.util.Map
Collection :单列集合的跟接口,他的两个重要的子接口java.util.List和java.util.Set;
List特点是元素有序,元素可重复;主要接口ArrayList和LinkedList。
Set特点是元素无序,并且不可重复。
8. Iterator迭代器
Java.util.Iterator也属于集合,但是主要用于迭代访问collection中的元素。
Public Iterator iterator():获取集合对应的迭代器,用来遍历集合中的元素;
常用的方法:
Public E next():返回迭代的下一个元素;
Public Boolean hasNext():如果有元素可以迭代,则返回true.
迭代器遍历集合的时候内部采用指针的方式跟踪集合中的元素,当调用next方法之前迭代器的索引位于第一个 元素之前,当第一次调用迭代器的next方法后,迭代器的索引会向后移一位,指向第一个元素并将第一个元素 返回,以此类推,直到hasNext方法返回false。
增强for循环(也称for each循环):是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。它的内 部原理其实是一个iterator迭代器,
格式:for(元素的数据类型 变量:集合或数组){//写操作代码}。
9. 泛型
泛型:可以在类或方法中预知的使用未知的类型,一般在创建对象的时候,将未知的类型确定具体的类型,没有指定泛型时默认类型为object类型。
好处:将运行时期的CassCastException,转移到了编译时期的编译失败。避免了类型强制转换的麻烦。泛型是数据类型的一部分,将类名和泛型一起看作数据类型。
泛型,用来灵活的将数据类型运用到不同的类、方法、接口中,将数据类型作为参数进行传递。定义格式:修饰符 class 类名<代表泛型的变量>{}
例如:API中Arraylist集合:
class ArrayList<E>{
public boolean add(E e){ }
public E get(int index){ }
....
}
使用泛型:即什么时候确定泛型。
在创建对象的时候确定泛型:ArrayList<String> list=new ArrayList<String>();
此时,变量E的值就是String类型,那么就可以理解为:
class ArrayList<String>{
public boolean add(String e){ }
public String get(int index){ }
...
}
举例自定义泛型类:
public class MyGenericClass<MVP> {
//没有MVP类型,在这里代表 未知的一种数据类型 未来传递什么就是什么类型
private MVP mvp;
public void setMVP(MVP mvp) {
this.mvp = mvp;
}
public MVP getMVP() {
return mvp;
}
}
使用:
public class GenericClassDemo {
public static void main(String[] args) {
// 创建一个泛型为String的类
MyGenericClass<String> my = new MyGenericClass<String>();
// 调用setMVP
my.setMVP("大胡子登登");
// 调用getMVP
String mvp = my.getMVP();
System.out.println(mvp);
//创建一个泛型为Integer的类
MyGenericClass<Integer> my2 = new MyGenericClass<Integer>();
my2.setMVP(123);
Integer mvp2 = my2.getMVP();
}
}
含有泛型的方法:定义格式:修饰符 <代表泛型的变量> 返回值类型 方法名(参数){}
例如:
public class MyGenericMethod {
public <MVP> void show(MVP mvp) {
System.out.println(mvp.getClass());
}
public <MVP> MVP show2(MVP mvp) {
return mvp;
}
使用格式:调用方法时确定泛型的类型。
public class GenericMethodDemo {
public static void main(String[] args) {
// 创建对象
MyGenericMethod mm = new MyGenericMethod();
// 演示看方法提示
mm.show("aaa");
mm.show(123);
mm.show(12.45);
}
}
含有泛型的接口:定义格式:修饰符 interface 接口名<代表泛型的变量>{}
例如:public interface MyGenericInterface<E>{
public abstract void add(E e);
public abstract E getE();
}
使用格式:
定义类型时确定泛型的类型:
public class MyImp1 implements MyGenericInterface<String> {
@Override
public void add(String e) {
// 省略...
}
@Override
public String getE() {
return null;
}
}
此时泛型E的值就是String类型
始终不确定泛型的类型,直到创建对象时确定泛型的类型:
public class MyImp2<E> implements MyGenericInterface<E> {
@Override
public void add(E e) {
// 省略...
}
@Override
public E getE() {
return null;
}
}
确定泛型:
/*
* 使用
*/
public class GenericInterface {
public static void main(String[] args) {
MyImp2<String> my = new MyImp2<String>();
my.add("aa");
}
}
泛型的通配符:当使用泛型类或接口时,传递的数据中,数据类型不确定,可以通过通配符<?>表示,但是一旦使用泛型的通配符之后,只能使用object 类中共性方法,集合中元素自身方法无法使用。不知道使用什么类型来接受的时候,此时可以使用? ,?表示未知通配符。此时只能接收数据,不能往该集合中存储数据。
public static void main(String[] args) {
Collection<Intger> list1 = new ArrayList<Integer>();
getElement(list1);
Collection<String> list2 = new ArrayList<String>();
getElement(list2);
}
public static void getElement(Collection<?> coll){}
//?代表可以接收任意类型
泛型不存在继承关系,Collection list=new ArrayList();这种是错误的。
通配符的高级使用:受限泛型
泛型的上限:
格式:类型名称<? Extends 类> 对象名称
意义:只能接收该类型及其子类。
泛型的下限:
格式:类型名称<? Super 类> 对象名称
意义:只能接收该类型及其父类型。
比如:现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类
public static void main(String[] args) {
Collection<Intger> list1 = new ArrayList<Integer>();
getElement(list1);
Collection<String> list2 = new ArrayList<String>();
getElement(list2);
}
public static void getElement(Collection<?> coll){}
//?代表可以接收任意类型
public static void main(String[] args) {
Collection<Integer> list1 = new ArrayList<Integer>();
Collection<String> list2 = new ArrayList<String>();
Collection<Number> list3 = new ArrayList<Number>();
Collection<Object> list4 = new ArrayList<Object>();
getElement(list1);
getElement(list2);//报错
getElement(list3);
getElement(list4);//报错
getElement2(list1);//报错
getElement2(list2);//报错
getElement2(list3);
getElement2(list4);
}
// 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(Collection<? extends Number> coll){}
// 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(Collection<? super Number> coll){}
10.数据结构
数据存储常用的结构有:栈、队列、数组、链表和红黑树。
栈:stack,又称堆栈,他是运算受限的线性表,其限制是只允许在标的一端进行插入和删除操作。
特点就是:先进后出,栈的入口出口位置都是栈的顶端。
压栈就是存元素,把元素存储到栈的顶端位置,已有的元素依次向栈底方向移动一个位置。
弹栈就是取元素,栈中已有元素依次向栈顶方向移动一个位置。
队列:queue,简称队,也是一种运算受限的线性表,其限制是只允许在表的一端进行插入,在表的另一端进行删除。
特点:先进先出,队列的入口、出口各占一侧。
数组:array是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。
特点:
查找元素快:通过索引可以快速访问指定位置的元素。
增删元素慢:
指定索引位置增加元素:需创建一个新数组,将指定新元素存储在指定索引位置, 再把源数组元素根据索引复制到新数组对应索引位置。
指定索引位置删除元素:需要创建一个新数组,把原数组元素根据索引,复制到新数组对应的索引位置,原数组指定索引位置不复制到新数组。
链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分,一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。我们常说的链表结构有单向链表和双向链表。
单向链表对元素的存取特点:
1) 多个结点之间通过地址进行连接,例如,多个人手拉手,每个人用自己的右手拉住下一个人的右手,依次类推,这样多个人就连在一起了。
2) 查找元素慢:像查找某个元素,需要通过连接的结点,依次向后查找指定元素
3) 增删元素快:
增加元素:只需要修改连接下个元素的地址即可。
删除元素:只需要修改连接下个元素的地址即可。
红黑树:二叉树,binary tree是每个节点不超过2的有序树(tree).
简单的理解,就是一种类似于生活中的树,只不过每个结点上最多只能有两个子结点,二叉树是每个结点最多有两个子树的树结构。顶上的叫根结点,两边的被称作“左子树”和“右子树”。二叉树也叫红黑树,就是一颗二叉查找树,将结点插入后,该树依然是一颗二叉查找树,也就意味着树的键值还是有序的。
红黑树的约束:
-
结点可以是红色的或者黑色的。
-
根节点是黑色的。
-
叶子结点(特指空结点)是黑色的。
-
每个红色结点的子节点都是黑色的。
-
任何一个结点到其每一个叶子结点的所有的路径上黑色结点数相同。
红黑树的特点:速度特别快,趋近平衡树,查找叶子元素最少和最多次数不多于二倍。
11.hashCode
1.hash和hash表是什么?
hash:即散列,把任意长度的输入(也叫预映射,per-image),变成固定长度的输出,该输出就是散列值;这种转换是一种压缩映射,也就是,散列值的空间远远小于输入的空间;不同的输入可能输出相同的散列值,不能以散列值的唯一来判断输入值的唯一。简单的说就是将任意长度的消息压缩到某一固定长度的消息摘要的函数。hash是一个函数,里面有多种算法,算法得来的就是hash值。
hash表:hash算法得来的hash值存放在hash表中,hash表就是所有hash值的组成。
2.hashCode
hashCode就是在hash表中有对应的位置,就是通过一种算法得到的。每个对象都有HashCode,通过对象的内部地址(也就是物理地址:对象放在内存中的地址)转换成一个整数,然后该整数通过hash函数的算法就得到了hashCode,所以hashCode就是在hash表中对应的位置。hashCode是对象的内存地址经过hash算法得出来的整数,重写前比较的其实就是对象的地址,重写后比较的是对象的属性转换成字符再由字符转换成的ASCII码值乘以一个数,然后累加起来。
3.hashCode的作用:主要是为了查找的快捷性。
4.equals方法和hashCode的关系
1.如果两个对象equals相等,那么这两个对象的HashCode一定也相同。
2.如果两个对象的HashCode相同,不代表两个对象就相同,只能说明两个对象在散列存储结构中,存放于同一位置。
5.HashCode()在HashSet和HashMap中的作用
import java.util.HashSet;
import java.util.Set;
public class Student {
private String num;
private String name;
public Student(String num, String name) {
this.num = num;
this.name = name;
}
@Override
public int hashCode() {
StringBuilder sb = new StringBuilder();
sb.append(num);
sb.append(name);
char[] charArr = sb.toString().toCharArray();
int hash = 0;
for(char c : charArr) {
hash = hash * 131 + c;
}
return hash;
}
public static void main(String[] args) {
Student stu1 = new Student("10001", "赤骥");
Student stu2 = new Student("10001", "赤骥");
Student stu3 = new Student("10002", "白义");
Set<Student> students = new HashSet<>();
students.add(stu1);
students.add(stu2);
students.add(stu3);
System.out.println(students.size());
}
}
这段代码的意思是重写hashCode方法后两个相同的对象还是放到了hashSet中;
hashSet源码:
1-public boolean add(E e) { return map.put(e, PRESENT)==null; }
2-private transient HashMap<E,Object> map;
3-public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
由代码可知,hashSet存值的底层是由HashMap的put方法实现的,其中putVal()这个方法会调用存入元素的hashCode()方法,计算出元素所对应在HASH表中的位置,然后判断这个位置是否已经有内容,如果这个位置有元素那么就待用
传入元素的equals()方法与已有的元素进行对比,以此来判断两个元素是否相同,如果不相同就将此元素也存入表中。
6.equals方法的作用
也就是说,使用hashCode()方法确定元素在数据结构中存放的位置。而使用equals()方法来确认当两个元素存放的位置发生冲突时,是应该将两个元素都存入数据结构还是只存一个。equals()重写前比较的是堆内存的地址。
12.List集合
1)list接口介绍
Java.util.List接口继承Collection接口,特点:
-List集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。
-他是一个带有索引的集合,通过索引可以精确的操作集合中的元素(与数组的索引是一个道理)。
-List集合允许出现重复元素,通过元素的equals方法来比较是否为重复元素。
2)List接口中常用的方法
-public void add(int index,E element):将指定元素添加到该集合的指定位置。
-public E get(int index):返回集合中指定位置的元素。
-public E remove(int index):移除指定位置的元素,返回的是被移除的元素。
-public E set(int index,E element):用指定元素替换集合中指定位置的元素,返回的是更新前的元素。
3)List的子类:
ArrayList集合:
Java.util.ArrayList:集合数据存储的结构是数组结构,元素增删慢,查找快。
LinkedList集合:
Java.util.LinkedList:集合存储的是链表结构,元素增删快,查找慢。是一个双向链表,找到头和尾非常方便,里面有大量操作首尾元素的方法。
-public void addFirst(E e):将指定元素插入此列表的开头。
-public void addLast(E e):将指定元素添加到此列表的结尾。
-public E getFirst():返回此列表的第一个元素。
-public E getLast():返回此列表的最后一个元素。
-public E removeFirst():移除并返回此列表的第一个元素。
-public E removeLast():移除并返回此列表的最后一个元素。
-public E pop():从此列表所表示的堆栈处弹出一个元素。
-public void push(E e):将元素推入此列表表示的堆栈。
-public Boolean isEmpty():如果列表不包含元素,则返回true.
13.Set集合
Set集合有多个子类,元素是无序的,并且是不能重复的,这里介绍java.util.HashSet和java.util.LinkedHashSet。
Java.util.HashSet是set接口的一个实现类,它所存储的元素是不可重复的,并且元素是无序的(即存取顺序不一致)。Java.util.HashSet底层的实现其实是一个java.util.HashMap支持。
Hashset是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能,保证元素唯一性的特性依赖于:hashcode和equals方法。
Hashset存储数据的结构(哈希表):在jdk1.8之前哈希表底层采用数组+链表实现,即使用链表处理冲突,同一哈希值的链表都存储在一个链表里,但是当位于同一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低,而jdk1.8中哈希表存储采用数组+链表+红黑树实现,当链表长度超过阀值(8)时,将链表转为红黑树,这样大大减少了查询时间。
HashSet存储自定义类型元素:给Hashset中存储自定义类型的元素,需要重写对象中的hashcode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一。
public class Student {
private String name;
private int age;
public Student() { }
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class HashSetDemo2 {
public static void main(String[] args) {
//创建集合对象 该集合中存储 Student类型对象
HashSet<Student> stuSet = new HashSet<Student>();
//存储
Student stu = new Student("于谦", 43);
stuSet.add(stu);
stuSet.add(new Student("郭德纲", 44));
stuSet.add(new Student("于谦", 43));
stuSet.add(new Student("郭麒麟", 23));
stuSet.add(stu);
for (Student stu2 : stuSet) {
System.out.println(stu2);
}
}
}
执行结果:
Student [name=郭德纲, age=44]
Student [name=于谦, age=43]
Student [name=郭麒麟, age=23]
LinkedHashSet:java.util.LinkedHashSet是链表和哈希表结合的数据存储结构,可以保证元素有序。
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<String>();
set.add("bbb");
set.add("aaa");
set.add("abc");
set.add("bbc");
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
结果:bbb
aaa
abc
bbc
可变参数:在jdk1.5之后定义一个方法需要接收多个参数,并且多个参数类型一致,可以对其简化成如下格式:修饰符 返回值类型 方法名(参数类型…形参名){}
等价于:修饰符 返回值类型 方法名(参数类型[] 形参名){}
只是后面这种定义,在调用是必须传递数组,而前者可以直接传递数据即可。
Jdk1.5之后,出现了简化操作。… 用在参数上称之为可变参数
public class ChangeArgs {
public static void main(String[] args) {
int[] arr = { 1, 4, 62, 431, 2 };
int sum = getSum(arr);
System.out.println(sum);
// 6 7 2 12 2121
// 求 这几个元素和 6 7 2 12 2121
int sum2 = getSum(6, 7, 2, 12, 2121);
System.out.println(sum2);
}
/*
* 完成数组 所有元素的求和 原始写法
public static int getSum(int[] arr){
int sum = 0;
for(int a : arr){
sum += a;
}
return sum;
}
*/
//可变参数写法
public static int getSum(int... arr) {
int sum = 0;
for (int a : arr) {
sum += a;
}
return sum;
}
}
如果在方法书写时,这个方法有多参数,参数中包含可变参数,可变参数一定要写在参数列表的末尾位置。
14.Collections
Java.util.Collections是集合工具类,用来对集合进行操作,部分方法如下:
-public static <T> Boolean addAll(Collection<T> c,T…elements):往集合中添加一些元素
-public static void shuffle(List<?> list):打乱集合顺序
-public static <T> void sort(List<T> list):将集合中的元素按照默认规则排序
-puclic static <T> void sort(List<T> list,Comprator<? Super T>):将集合中的元素按照指定规则排序。
Comparator**比较器:**
Java中提供了两种比较实现的方式:java.lang.Comparable接口实现不灵活,java.lang.Comparator接口实现比较灵活。
采用public static <T> void sort(List<T> list) 这个方法完成的排序,实际上要求了被排序的类型需要实现Comparable接口,重写comparTo()方法,完成比较的功能,在String类型上如下:
-public final class String implements java.io.Serializable,Comparable<String>,CharSequence{}
String类实现了这个接口,并完成了比较规则的定义,但是这样就把这种规则写死了,这时候可以使用-public static <T> void sort(List<T> list,Comparator<? Super T>)方法灵活的完成,这个里面就涉及到了Comparator这个接口,位于java.util包下,排序是comparator能实现的功能之一,该接口代表一个比较器,比较器具有可比性,顾名思义就是排序用的,通俗的讲需要比较两个对象谁排在前谁排在后面,比较的方法就是:
-public int compare(String o1,String o2):比较两个参数的顺序;
两个对象比较的结果有三种:大于、等于、小于。
Return o1-o2 升序,反之降序
如果按照升序排序,则o1小于o2,则返回负数,相等返回0,o1大于o2返回正数;如果按照降序排序,o1小于o2返回正数,相等返回0,o1大于02返回负数。
public class CollectionsDemo3 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("cba");
list.add("aba");
list.add("sba");
list.add("nba");
//排序方法 按照第一个单词的降序
Collections.sort(list, new Comparator<String>() {
15.Map
-public interface Map<K,V>将键映射到值的对象,一个映射不能包含重复的键,每个键最多能映射到一个值。
Map**常用子类**:
-HashMap:存储数据采用哈希表结构,查询速度比较快,元素的存取顺序不能保证一致,由于要保证键的唯一、不重复,需要重写hashCode()方法、equals()方法;
-LinkedHashMap:HashMap下有个子类LinkedHashMap,存储数据采用哈希表结构+链表结构。通过链表结构可以保证元素的存取一致,通过哈希表结构可以保证键的唯一、不重复,需要重写键的hashCod()方法和equals()方法。
常用的方法:
-public V put(K key,V vaue):把指定的键和指定的值添加到Map集合中,key不重复,返回null,key重复,会使用新的value替换已有的value返回被替换的value;
-public V remove(Object key):把指定的键所对应的键值对元素在集合中删除,返回被删除元素的值。
-public V get(Object key):根据指定的键,获取对应的值;
-public Set<K> keyset():获取结合中所有的键,存储在set集合中。
-public Set<Map.Entry<K,V>> entrySet():获取到Map集合中所有的键值对对象的集合。
Map**集合遍历键找值的方式**:
Set<String> keys=map.keySet();//获取所有的键
//遍历键集得到每一个键
-for(String key:keys){
//key就是键
String value=map.get(key);
}
Entry 键值对对象:
Map中的键和值是一一对应的关系,这一对对象称作Map中的一个Entry项。Entry将键值对的对应关系封装成了对象,即键值对对象,这样在遍历map集合时就可以从每一个键值对(entry)对象中获取对应的键和对应的值。
-public K getKey():获取entry对象中的键
-public V getValue:获取entry对象的值
Map中获取所有entry对象的方法:public Set<Entry<K,V>> entrySet();
Set<Entry<String,String>> entrySet=map.entrySet();
For(Entry<String,String> entry:entrySet){
String value=entry.getValue();
String key=entry.getKey();
}
注意:当给hashmap中存放自定义对象时,如果自定义对象作为key值时,这时要保证对象唯一,必须重写对象的hashcode和equals方法。
LinkedHashMap:可以保证元素有序,并且查询速度快,它是链表和哈希表组合的数据存储结构。用法和hashmap一样。Map.containsKey(e)返回true说明key已存在。
16.异常
异常:指的是程序在执行过程中,出现非正常的情况最终会导致JVM非正常停止。在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出异常对象。Java处理异常的方式是中断处理。
异常机制其实是帮我们找到程序中的问题,异常的根类是java.lang.Throwable,其下有两个子类:java.lang.Error(不能处理,只能避免)与java.lang.Exception(由于使用不当,可以避免的),通常所说的是:java.lang.Exception。
Throwable中常用的方法:
-public void printStackTrace():打印异常信息,包含了异常的类型,异常原因,还包括异常出现的位置,在开发和调试阶段都得使用printStackTrace().
-public String getMessage():获取发生异常的原因。提示给用户的时候就提示错误原因。
-public String toString():获取异常的类型和异常描述信息(不用)。
分为编译时异常和运行时异常。
异常的处理:
Java异常处理的五个关键字:try、catch、finally、throw、throws
Throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。使用格式:throw new 异常类名(参数)。
Objects**非空判断**:
Objects 由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或null-tolerant(容忍空指针的),源码中其对对象为null的值进行了抛出异常操作,
-public static <T> T requireNonNull(T obj):查看指定引用对象不是null.
声明异常throws**:**关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者处理异常。
异常声明格式:修饰符 返回值类型 方法名(参数)throws 异常类名1,异常类名2…{}
public class ThrowsDemo {
public static void main(String[] args) throws FileNotFoundException {
read("a.txt");
}
// 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw
throw new FileNotFoundException("文件不存在");
}
}
}
若该方法有多种异常可能发生,throws后面可以写多个异常类用逗号隔开。
public class ThrowsDemo2 {
public static void main(String[] args) throws IOException {
read("a.txt");
}
public static void read(String path)throws FileNotFoundException, IOException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw
throw new FileNotFoundException("文件不存在");
}
if (!path.equals("b.txt")) {
throw new IOException();
}
}
}
捕获异常try…catch:
Finally代码块:无论是否出现异常都会执行。不能单独使用,必须和try一起使用,一般用于资源释放。
自定义异常:继承Exception或RuntimeException,带参构造函数,调用父类的带参构造函数。
public RegisterException(String message) {
super(message);
}
17.多线程
1)并发与并行
并发:两个或多个事件在同一个时间段内发生。
并行:两个或多个事件在同一时刻发生(同时发生)。
在操作系统中安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单CPU系统中每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行。在多个cpu的系统中,则这些可以并发执行的程序可以分配给多个处理器(CPU),实现多任务并行执行,即每个处理器来处理一个可以并发执行的程序,并行处理的程序越多电脑运行的效率越高。
注意:单核处理器的计算机肯定不能处理多个任务的,只能是多个任务在单个CPU上并发运行。同理线程也是一样的。
2)线程与进程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即一个进程从创建、运行到消亡的过程。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程;一个进程中可以有多个线程,这个应用程序也可以称之为多线程程序。
3)创建线程类
Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务。实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
通过继承Thread**类来创建并启动多线程**:
1. 定义Thread类的子类,并重写该类的run方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程的执行体。
2. 创建Thread子类的实例,即创建了线程对象
3. 调用线程对象的start()方法来启动该线程。
Thread**类的构造方法**:
-public Thread():分配一个新的线程对象;
-public Thread(String name):分配一个指定名字的新的线程对象;
-public Thread(Runnable target):分配一个带有指定目标新的线程对象;
-public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。
常用方法:
-public String getName():获取当前线程名称;
-public void start():导致此线程开始执行,Java虚拟机调用此线程的run()方法。
-public void run():此线程要执行的任务在此处定义代码。
-publid static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止)。
-public static Thread currentThread():返回对当前正在执行的线程对象的引用。
实现Runnable**接口创建多线程**:
1. 定义Runnable接口的实现类,并重写接口的run()方法;该run()方法的方法体同样是该线程的线程执行体。
2. 常见Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
3. 调用线程对象的start()方法启动线程。
通过实现Runnable接口,使得该类有了多线程类的特征,run()方法是多线程程序执行的目标,所有的多线程代码都在run方法里面。Thread类实际上也是实现Runnable接口的类。
在启动多线程的时候需要通过Thread构造方法Thread(Runnable target)构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
注意:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run方法。
Thread**和Runnable的区别**:如果一个类继承Thread,则不适合资源共享,实现Runnable接口很容易实现资源共享。
总结:实现Runnable接口比继承Thread类所既有的优势:
1. 适合多个相同的程序代码的线程去共享一个资源。
2. 可以避免java中单继承的局限性。
3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
4. 线程池只能放入实现Runnable或Callable类线程不能直接放入继承Thread的类。
在Java中每次程序至少启动两个线程,一个是main线程,一个是垃圾收集线程,因为每当使用java命令执行一个类的时候,实际上都会启动一个jvm,每一个jvm其实就是在操作系统中启动了一个进程。
匿名内部类方式实现线程的创建:
匿名内部类的作用:简化代码
把子类继承父类,重写父类方法,创建子类对象一步合成;
把实现类实现接口,重写接口方法,创建实现类对象一步合成。
匿名内部类的最终产物:子类/实现类对象,而这个类没有名字。
格式: new 父类/接口(){
重写父类/接口中的方法;
}
18.线程安全
多线程访问了共享数据就会产生线程安全问题,没有访问共享数据是不会产生线程安全问题的。
注意:线程安全问题是不能产生的,我们可以让一个线程在访问共享数据的时候,无论是否失去了cpu的执行权,让其他的线程只能等待,等待当前线程卖完票,其他线程再进行卖票。线程安全都是由全局变量及静态变量引起的,若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说这个全局变量是线程安全的,若有多个线程执行写操作,一般都要考虑线程同步,否则的话就有可能影响线程安全。
线程同步:
当使用多个线程访问同一资源的时候,且多个线程对资源有写的操作,就容易出现线程安全问题。
有三种方式完成同步操作:
-
同步代码块;
-
同步方法;
-
锁机制。
同步代码块:synchronized关键字可以用于方法中的某个区块中,表示只对这个区块中的资源实行互斥访问。
格式:synchronized(同步锁){ 需要同步操作的代码 }。
同步锁:对象的同步锁只是一个概念,可以想象为对象上标记了一把锁。
锁对象可以是任意类型;多个线程对象要使用同一把锁。任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED).
同步技术的原理:使用了锁对象,也叫同步锁、锁对象或者对象监视器。当一个线程抢到了cup的执行权,执行run方法,遇到synchronized代码块,这是线程会检查synchronized代码块是否有锁对象,发现有,就获取锁对象进入到同步代码块中执行;同样的第二个线程抢到了cpu执行权,遇到同步代码块时发现没有锁对象(这时第一个线程没有执行完同步代码块,正在拥有锁对象),然后进入阻塞状态,一直等到第一个线程执行完同步代码块中的代码,把锁对象归还给同步代码块,第二个线程获取锁对象进入到同步中执行。
总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。
同步方法:使用synchronized修饰的方法,就叫同步方法,保证一个线程执行该方法的时候,其他线程只能在方法外等着。
格式:public synchronized void mrthod(){可能会产生线程安全问题的代码}
同步锁:对于非static方法,同步锁就是this,就是实现类对象 new RunnableImpl().
对于sataic方法,我们使用当前方法所在类的字节码对象(类名.class)
LOCK锁:java.util.cconcurrent.locks.Lock提供了比synchronized代码块和synchronized方法更广泛的锁定操作,Lock锁也称同步锁,加锁与释放锁方法优化了:
-public void lock():加同步锁;
-public void unlock():释放同步锁。
Java.util.concurrent.locks.ReentrantLock implements Lock接口
使用步骤:
-
在成员位置创建一个ReentrantLock对象。
-
在可能出现线程安全问题的代码前调用Lock接口中的方法lock获取锁。
-
在可能出现线程安全问题的代码后调用Lock接口中的方法unlock释放锁。
19.线程的状态
在javaAPI中java.lang.Thread.State这个枚举中给出了六种线程状态;
-
new 新建状态:线程刚被创建,但是未被启动还没第哦啊用start方法。
-
Runnable 可运行状态:线程可以在Java虚拟机中运行的状态,可能正在运行自己的代码,也可能没有,这取决于操作系统处理器。
-
Blocked 锁阻塞状态:当一个线程试图获取一个对象锁,而该对象锁被其他线程持有,则该线程进入Blocked状态;当该线程持有对象锁时,该线程变成Runnable状态。
-
Waiting 无限等待:一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或notifyAll方法才能唤醒。
-
TimedWaiting 计时等待:同waiting状态,有几个方法有超时参数,调用他们将进图TimedWaiting状态。这一状态将一直保持到超时期满或者接收道唤醒通知。带有超时参数的方法有Thread.sleep、Object.wait,wait方法时Object类中的方法。
-
Terminated 被终止:因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡,或者调用stop方法。
计时等待TimedWaiting:
常用的时调用Thread.sleep(long)方法,可以单个线程调用,不必多个线程协作;将Thread.sleep()的调用放在run()之内,这样才能保证该线程执行过程中会休眠。Sleep与锁无关,线程睡眠到期自动苏醒,并返回到runnable可运行状态。
Blocked 锁阻塞**:一个线程在等待另一个线程释放对象锁的过程就是锁阻塞。
Waiting 无限等待:一个线程无限期等待另一个线程执行一个特别的动作(唤醒)的线程处于这一状态。
一个调用了某个对象的Object.wait方法的线程会等待另一个线程调用此对象的Object.notify()方法或Object.notifyAll()方法。Waiting状态并不是一个线程的操作,它体现的时多个线程间的通信。
只有锁对象才能调用wait和notify方法。
-void wait():在其他线程调用此对象的notify()方法或notifyAll()方法前,导致当前线程等待,同时会失去同步锁。
-void notify():唤醒在此对象监视器上等待的单个线程,会继续执行wait方法之后的代码。
20.等待唤醒机制
1.线程间通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
比如:线程A用来生成包子,线程B用来吃包子的,包子可理解为同一资源,线程A与线程B处理的动作,一个是生产,一个时消费,那么线程A和线程B之间就存在线程通信问题。重点是有效利用资源(生产一个消费一个)。
2.等待唤醒中的方法:
-wait:线程不再活动,不再参与调度,进入wait set 中,因此不会浪费cpu资源,也不会竞争锁了。这时线程 的状态即waiting。他还要等待别的线程进行一个特别的动作,也即是通知(notify)在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(reday queue)中。
-notify:则选取所通知对象的wait set中的一个线程释放(如等候最久的线程先释放)。
-notifyAll:则释放所有通知对象的wait set上的全部线程。
一个线程从等待中被唤醒,需要再次尝试去获取锁(因为当初中断的地方是在同步块内,而此刻它已经不再持有锁),获取锁成功后才能在当初调用wait方法之后地方恢复执行。
总结:如果能获取锁,线程就从WAITING状态变成RUNNABLE状态;否则,从wait set出来,又进入到entry set,线程从WAITING状态变成BLOCKED状态。
3.调用wait和notify方法需要注意的细节
1)wait方法和notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用wait方法后的线程。
2)wait方法和notify方法是属于Object类的方法。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
3)wait方法与notify方法必须要在同步代码块或者同步函数中使用。因为:必须通过锁对象调用这两个方法。
4.生产者与消费者问题
两个线程必须有一个线程在执行,锁对象必须唯一,可以用公共资源类作为锁对象。wait方法和notify方法只能有一个在执行。创建一个公共资源类和两个线程类,公共资源类的实例作为两个线程的锁对象。
21.线程池
线程池:就是一个容器-->集合(ArrayList,HashSet,LinkedList<Thread>,hashMap)
当程序第一次启动的时候,创建多个线程,保存到一个集合中;
当我们想要使用线程的时候,就可以从集合中取出线程来使用,取出的方法是Thread t=list.remove(0)返回的是被移除的元素(线程只能被一个任务使用);如果用的是linkedlist集合:Thread t=linked.removeFirst().
当我们使用完毕线程,需要把线程归还给线程池:list.add(t),linked.addLast(t)。
JDK1.5之后,jdk内置了线程池,可以直接使用。
使用线程的好处:1.降低资源消耗,减少了创建和销毁线程的次数。2.提高响应速度。3.提高线程的可管理性。
线程池的使用:java.util.concurrent.Executors是一个常见线程池的工厂类;Executors类中的静态方法:static ExecutorService newFixedThreadPool( int nThread)创建一个可重用固定线程数的线程池;参数:int nThread:创建线程池中包含的线程数量。返回值:ExecutorService接口,返回的是ExecutorService接口的实现类对象,可以使用ExecutorService接口接收(面向接口编程)。
java.util.concurrent.ExecutorService: 线程池接口,用来从线程池中获取线程,调用start方法执行线程任务;submit(Runnable task)提交一个runnable任务用于执行;关闭/销毁线程的方法:void shutdown().
线程池的使用步骤:
1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定数量的线程池。
ExecutorService es=Executors.newFixedThreadPool(2);
2.创建一个类,实现Runnable接口,重写run方法,设置线程任务。
3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法。
es.submit(new RunnableImpl());
4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)。
/*创建线程池的方法,返回线程池对象*/
public static ExcutorService newFixedThreadPool(int nThread){}
/*使用线程池对象的方法:获取线程池中的某一个对象并执行
Future接口:用来记录线程任务执行完毕后产生的结果,线程池创建与使用。
*/
public Future<?> submit(Runnable task){}
22.Lambda表达式
面向对象的思想:做一件事,找一个能解决这个事情的对象,调用这个对象的方法,完成事情。
函数式编程思想:只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程。
JDK1.8中加入了Lambda表达式。
Lambda表达式的标准格式:由三部分组成:一些参数、一个箭头、一段代码。
格式:(参数列表)->{一些重写方法的代码}
解释说明:
():接口中抽象方法的参数列表,没有参数就空着;有参数就写出来,多个参数用逗号隔开。
->:传递的意思,把参数传递给方法体{}
{}:重写接口的抽象方法的方法体
import java.util.Arrays;
public class Demo07ComparatorLambda {
public static void main(String[] args) {
Person[] array = {
new Person("古力娜扎", 19),
new Person("迪丽热巴", 18),
new Person("马尔扎哈", 20) };
Arrays.sort(array, (Person a, Person b) ‐> {
return a.getAge() ‐ b.getAge();
});
for (Person person : array) {
System.out.println(person);
}
}
}
Lamdba表达式 是可推导,可省略:凡是上下文推导出来的内容都可以省略。
可以省略内容:
1.(参数列表):括号中数据列表的参数类型,可以省略不写
2.(参数列表):括号中参数如果只有一个,那么数据类型和()都可以不写。
3.{一些代码}:如果{}中的代码只有一行,无论是否有返回值,都可以省略({},return,分号),注意:必须一起省略。
Lambda使用的前提:
1.使用Lambda必须有接口,且要求接口中有且仅有一个抽象方法。无论是JDK内置的Runable、Comparator接口还是自定义的接口,只有接口中的抽象方法存在且唯一的时候,才能使用Lambda。
2.使用Lambda必须有上下文推断。也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。有且仅有一个抽象方法的接口,称为“函数式接口”。
23.volatile关键字
作用:可以保证可见性,有序性,但不能保证原子性。
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
没有依赖关系的语句处理器会为了提高效率进行指令重排,指令重排不会影响单线程程序,但是会影响多线程。
当写双重检测锁版本的单例模式时,就要用到volatile来保证可见性
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
24.synchronized锁升级过程
1.synchronized使用方式:能同时保证可见性,有序性,原子性。
synchronized可以用在以下地方:
1.修饰实例方法,对当前实例对象this加锁。
3.修饰代码块,指定加锁对象,对给定对象加锁。
synchronized实现原理:
Java对象组成:对象放在堆内存中,可分为三部分,分别是对象头、实例变量和填充字节。
https://blog.csdn.net/zzti_erlie/article/details/103997713