JavaSE知识点总结(持续更新,欢迎指正)

基础

jvm,jdk,jre

1.字节码

.class文件,相对二进制文件是一种中间文件,只能在虚拟机上运行,也是java可移植性的由来。

2.jvm

Java 虚拟机(JVM)是运行 Java 字节码的虚拟机,会把字节码变成二进制机器码。JVM 有针对不同系统的特定实现(Windows,Linux,macOS)。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

3.jdk和jre:

顾名思义,jdk为工具,javac命令等。jre为运行环境

jdk包含jre,运行程序只要jre,开发需要jdk。

那些东西会被继承

1.成员变量和成员方法

父类private的不会被继承。

private以上都可被继承

2.类变量和类方法

同成员变量

3.构造方法

构造函数不能继承

ps:无法继承的东西是拥有的,只是无法访问

重载和重写,多态

1.重载

方法名必须相同(显然)

三者必有一:参数列表必须有不同之处(顺序,个数,类型)(重载的意义所在

方法返回值和访问修饰符可以不同。(扩展)

2.重写

方法名必须相同(显然)

参数列表必须相同(和重载相反处

访问修饰符范围大于等于父类;(不然会影响向上转型实现多态

插播多态:

Father father = new Son();
father.method();
1.这里多态体现在哪?
改变赋值为Father father=new Son1()
同样执行father.method();会根据father指向子类不同而多态,method方法的内容呈现多态
继承和接口都能实现多态,手段都是向上转型
2.若method权限被降低,father就不能调用method方法了,向上转型就废了

如果父类方法访问修饰符为 private 则子类就不能重写该方法。(因为根本不继承)

返回值范围小于等于父类,抛出的异常范围小于等于父类(里氏代换原则)

拆箱装箱

Interger it=new Interger(i); 装箱

int i=it.intvaule(); 拆箱

区别是基本类型和引用类型的区别,引用类型有避免空指针的好处

==的自动拆箱问题

例如

 1 public class Main {
 2     public static void main(String[] args) {
 3 
 4         Integer i1 = 100;
 5         Integer i2 = 100;
 6         Integer i3 = 200;
 7         Integer i4 = 200;
 
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        System.out.println(c==(a+b));//true

 9         System.out.println(i1==i2);  //true
10         System.out.println(i3==i4);  //false
11     }
12 }

总结

1.Integer类创建对象的时候,数值在[-128,127]之间,会返回cache数组中的已经存在的对象的引用。

2.包装类的"= ="运算只在遇到算术运算的情况下会自动拆箱,如a==b+c

3.当a和b在[-128,127],a= =b也可能成立(a在数值上=b),看起来和2矛盾,但是结合1即知是因为是返回缓存的地址。

== 与 equals

对象的==和equals

1.==

基本数据类型比较的是值,引用数据类型比较的是内存地址

2.equals

**没被重写等效==,**重写了可以实现别的效果。

我们很熟悉的equals判断字符串内容就是String类重写了方法。

hashCode 与 equals

上面说equals判断字符串内容就是String类重写了equals方法,有那么简单吗?实际上重写 equals 时还必须重写 hashCode 方法!

1.我们先介绍hashCode

1.hashCode() 是 Object.类的方法,作用是获取对象的哈希码。

2.哈希码是hashCode() 对堆上的对象产生独特值,利用的是内存地址。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等。

2.为什么要hashcode?

假设,HashSet中已经有1000个元素。当插入第1001个元素时,需要怎么处理? “将第1001个元素逐个的和前面1000个元素进行比较”?显然,这个效率是相等低下的。

HashSet 如何检查重复:

它使用哈希算法根据元素的hashcode计算出元素在散列表中的位置,如果位置上有对象,进行equals比较(判断对象是否真的相同),因为不同的key求hash是可能产生hash碰撞的),两次判断都相同才视为相同对象(equals不同会放到一个桶内)成功插入,否则不插入。这样的方法避免了一个大循环,速度upup。

简而言之hashcode只是在散列表中应用来缩短查找时间(求hash的时间复杂度是O(1),比遍历快多了),判重还得看equals,对象hashcode相同并不一定相同,可能哈希冲突。

3.为什么重写 equals 时必须重写 hashCode 方法

简单地说就是保证两者判重的逻辑一致即分别调用equals和hashcode时要返回结果相同(只需要对象会用Hashset存储时才有这个要求)

看hashmap源码可知散列表是会通过hashcode确定数组下标,再用equal判重的。假设你认为a和b是同一个对象,但是只重写equal使两者相同,而hashcode不同。根据hashmap源码连数组下标都不会一样,根本轮不到equal方法调用就会被Hashset当成不同的对象,和你认为的矛盾。

举例:当我们重写equals为属性name相同就相同时,我们hashcode方法应该重写为name相同hashcode就相同。

java值引用

java只有值引用,基本数据类型传递值,引用类型传递引用,都是传递值。

深拷贝 vs 浅拷贝

1.深拷贝,涉及new开辟内存,拷贝内存数据

new b

b.1=a.1;

b.2=a.2;

2.浅拷贝,拷贝引用

a=b

3.ps:集合框架可通过new(tmp)实现深拷贝,这是源码帮我们实现的

4.clone方法

对当前对象浅拷贝。

那么如何深拷贝呢?(1比较方便)

1.序列化这个对象再序列化回来,引出5

2.重写clone方法

5.什么是序列化

即将对象写到IO流中,变成字节的序列

好处是:这些字节序列相比对象

1.能保存在磁盘上

2.可以通过网络传输(所有网络上传输的对象都需要能序列化

6.为什么实现Serializbale接口就能够进行序列化?

Serializbale接口和RandomAccess接口原理基本相同,起标识类的作用。别的方法使用这些类的时候,会用instanceof检查是不是标识类的子类,然后采取不同的措施。

1.对于Serializbale,不是则报错

2.对于RandomAccess,不是则不会使用随机存储访问的方法,换用链式储存的方法

字符型常量和字符串常量的区别?

  1. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。换句话,char是基本数据类型,而String是引用数据类型。
  2. 注意: char 在 Java 中占两个字节

String对象创建过程

当创建 String 类型的对象时,虚拟机会在==常量池(因为String字符串内容是不变的)==中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

Object对象有那些方法(看过源码)

1.clone方法

实现Cloneable接口才能调用该方法

2.getClass

运行时获得类型

3.toString

String地址

4.finalize

用于释放资源,很少用,因为垃圾回收时必执行一次,释放两次资源会报错

5.equls和hashcode

6.wait,notify,notifyAll

当前线程等待该对象的锁,还可以wait(Long timeout)

唤醒对象上等待的某个进程

唤醒对象上等待的所有进程

notify等关于线程的方法为什么放在了object类里?

任何对象都可以被多个线程竞争

StringBuffer,StringBuilder,String对比

1.String

表面:
String的特点是不变性,即String对象的字符串内容是不变的,对String进行操作的函数实质都是产生了一个新的String对象,包括字符串的拼接。

经典考题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vnmnenTk-1597201197982)(file:///C:\Users\zzz\Documents\Tencent Files\657133058\Image\Group2\JM\M[\JMM[4U6_{EXZRA4$0{T8}`H.jpg)]

答:abc-----xyz

源码:String类中使用final关键字修饰字符数组来保存字符串的内容,所以其是不可变的不可继承的,因为内容被声明成了常量。

2.StringBuffer和StringBuilder比较,和spring比较

相同:
这两种对象都是可变的,原因自然是源码和String相反,没有使用final修饰字符数组。都继承AbstractStringBuilder类,所以API也大致相同。

不同:
线程安全性:buffer是线程安全的,其方法加了同步锁,而builder是线程不安全的。不过自然builder效率略快一些。

和String比:
显而易见有带缓冲区的优势,这个优势也决定了StringBuffer和StringBuilder更加适合大字符串,因为String对象老是会new一个新的String,对大字符串很不友好。反之小量字符串适合String

总之

大数据单线程StringBuilder

大数据多线程StringBuffer

小数据spring

3.StringBuffer的API

1.构造方法
· public StringBuffer() :无参构造方法
· public StringBuffer(int capacity) :指定容量的字符串缓冲区对象
· public StringBuffer(String str) :指定字符串内容的字符串缓冲区对象
2.添加功能
· public StringBuffer append(String str):可以把任意类型数据添加到字符串缓冲区里面,并返回字符串缓冲区本身
· public StringBuffer insert(int offset,String str):在指定位置把任意类型的数据插入到字符串缓冲区里面,并返回字符串缓冲区本身
3.删除功能
· public StringBuffer deleteCharAt(int index):删除指定位置的字符,并返回本身
· public StringBuffer delete(int start,int end):删除从指定位置开始指定位置结束的内容,并返回本身
4.替换功能
· public StringBuffer replace(int start,int end,String str):使用给定String中的字符替换词序列的子字符串中的字符
5.反转和截取:略

super和this

this

1.调用当前实例方法

this()调用对象无参构造函数

2.调用当前实例变量

3.除了和方法参数冲突的情况都是可省的

super

1.调用父类构造方法

super()无参

super(10):自动搜索父类匹配的构造

2.用于访问父类成员变量 number ,调用其父类 的方法。

super.number = 10;

super.showNumber();

Final Finally Finalize

final:

1.类final不可被继承,原理是方法和成员都隐性加上final

2.变量final意味是常量,且必须赋初值

3.方法final不可重写

4.private不可继承的原因可以说是被隐式指定为了final

Finally

try catch后面总是会执行的代码块,可以把释放资源写在这里。注意就算别的地方return都还是会先执行finally块再return

finalize

Object类的方法,垃圾收集器在将对象从内存里面回收时调用的方法,做一些清理工作。重写之可以整合系统其他资源或做一些别的清理工作。

Comparable和Comparator的区别

1.Comparable:内部比较器

继承这个接口后,重写实现compareTo方法,放入treemap等有序集合中即会自动排序。

2.Comparator:外部比较器

类无法修改时,可以使用此外部比较器(内部比较器则要修改类的源代码)。此时在treemap等有序集合中就会通过compare方法的规则重排序。

用法:先sort方法动态绑定这个外部比较器(这里用的Collections工具类,对象.sort也可以)

Collections.sort(arrayList, new Comparator() { @Override            
public int compare(Integer o1, Integer o2) {
    return o2.compareTo(o1);            
}        
});

3.集合里面排序的规则

规则:compareTo/compare方法return负数代表降序,正数代表升序。

内部类

广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。

1.匿名内部类的Lambda(常用)

基本的很熟悉了,就是new 接口/抽象类,然后实现方法。讲一下和Lambda结合的方法。Lambda表达式允许使用更简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例,完全可以用于简化匿名内部类,因为其方法很多情况only one。

interface C{
    public void getData(int e);
}

public class Anonymous {
    public void test1(A a){
        System.out.println("interface A 的getNum是:  ");
        a.getNum();
    }
    public void test2(C c){
        System.out.println("interface C 的getData是:  " );
        c.getData(10);
    }
    public static void main(String[] args){
        Anonymous an = new Anonymous();
       
       an.test1(new A() {//匿名内部类实现
            public void getNum() {
                System.out.println("A 实现");
            }
        });
        an.test2(new C() {//匿名内部类实现
            public void getData(int e) {
                System.out.println("C 实现");
            }
        });
        
        //Lambda表达式,形参表为空,代码体中直接写方法体
        an.test1(()->{
            System.out.println("A 实现");
                });
        //Lambda表达式,有形参写->前面
        an.test2((c)->{
            System.out.println("C 实现");
        });
    }
}

Lambda表达式更简洁的本质是规定了被继承的接口抽象方法只有一个,那么创建的时候很多东西都确定了,以test2为例子,test2方法参数为接口C,所以类名省略,接口C只有一个方法,所以除了方法体外都省略,剩下的只有具体参数和方法体。

实际应用举例

外部比较器:

 strList.sort((s1, s2) -> (s1 + s2).compareTo(s2 + s1));

等效

strList.sort(new Comparator() {            
    public int compare(String o1, String o2) {
        return (o1 + o2).compareTo(o2 + o1);            
    }   
});

ps:Lambda代码块只有一条return语句,甚至可以省略return关键字。如果代码块只有一条语句,Lambda表达式会自动返回这条表达式的值。所以这里return也省略了。

2.成员内部类

class Circle {    
	double radius = 0;   
    
    public Circle(double radius) {        
    this.radius = radius;    
    }      
    
    class Draw {     
    //内部类        
        public void drawSahpe() {            		 				System.out.println("drawshape");        
        } 
    
    } 
    
}

1.类Draw像是类Circle的一个成员,Circle称为外部类。成员内部类可以无条件访问外部类的所有成员属性和成员方法

2.在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问

3.成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。

4.由于Draw类像一个成员,所以不同于类只有两种修饰权限,它有四种。和成员差不多:private 修饰,则只能在外部类的内部访问,如果用 public 修饰,则任何地方都能访问;如果用 protected 修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。

总是显式定义无参构造而不利用隐式的(super的扩展)

1.我们知道每一个子类的构造方法必调用super方法(帮助子类做初始化工作),先是看有无显式super去调用父类的特定构造。不显式的调用,编译也会自动插入super()调用父类无参构造的。

2.当类无构造方法时,隐式存在一个无参构造。

3.如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()来调用父类中特定的构造方法,则编译时将发生错误。因为我们总是习惯不调用super,所以父类总是显式定义一个无参构造

import java 和 javax 有什么区别?

javax 之前只是扩展 API 包,后面成为了默认的懒得改到java包了。两者都是java自带api。

接口和抽象类的区别是什么?

常规的:

1.方法

接口的方法默认是 public abstract,也只能如此(一般不写用默认)。而抽象类方法基本没有限制(除了不能private,甚至可以没有抽象方法)。abstract导致接口所有方法都必须无实现,而抽象类不同。

2.成员变量

抽象类基本无限制(也是不能private,接口和抽象类都是用来继承的private没有意义),接口默认是static且final,也只能如此

总结:抽象类除了能用abstract关键字,和不能用private外和普通父类无区别。而接口就严格得多,都有默认且必须

亮点:

  1. 在 jdk 7 或更早版本中,接口如上所述
  2. jdk8 的时候接口可以有默认方法体和静态方法。其静态方法实现类和实现是不可以调用的(不同正常静态方法)。
  3. Jdk 9 在接口中引入了私有方法和私有静态方法。

成员变量与局部变量的区别

1.存在区域:堆和栈

2.语法上:一个可static一个不可

3.生命周期

4.java要求变量必须有初值,成员变量可自动,局部变量必手动。但final 修饰的成员变量也必须显式地赋值,不可更改肯定不帮你自动赋值。

Java 序列化中如果有些字段不想进行序列化,怎么办?

对于不想进行序列化的变量,使用 transient (短暂的意思)关键字修饰。

transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法

高级特性

注解

Jdk5.0引入得新技术

作用:为程序添加额外的辅助信息,可以附加在各种地方上,class,method,package等。

常见注解有:@override:检查重写是否符合条件,检查型注解。另外两类为编写文档和代码分析型注解。

元注解

元注解的作用就是负责注解其他注解,类似元数据。

@Target //被描述的注解可以用在什么地方

@Retention //用于描述注解的生命周期

// SOURCE(源码) < CLASS(.class文件阶段 ) < RUNTIME(运行时) 即在什么时候有效,不用说框架都是RUNTIME。

@Document //该注解是否生成在JAVAdoc中

@Inherited //子类能继承父类的该注解

源码解析:

点开这四个注解源码,可以发现一个方法,其返回值都是一个对象数组,对象的类各不相同,我们点开类源码就知道这种@Target(value={?,?})里面的?可以填什么了

自定义注解

1.@interface用来声明一个注解,格式:public @interface 注解名{定义内容},自动继承了java.long.annotation.Annotation接口。

2.注解的方法:

例:ElementType[] value();一个方法实际上是对应一个注解参数

​ 1.方法的名称就是参数的名称

​ 2.返回值类型就是参数的数据类型

​ 3.注解的配置参数必须要有值,但明显很多注解我们都不写参数,所以我们定义注解元素时,经常使用空字符串,0作为默认值。default声明默认值。

​ 4.一般只有一个参数使用value作为名称,因为此时value可省略即@MyAnnotation({“zzz”,“ccc”}),不成文规定。

3.自定义注解例子

public class Test{
    @MyAnnotation(value={"zzz","ccc"})
    public void test() {}
}
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {
 	 //注解的参数:参数类型 + 参数名 ();
    //default :默认值
    String[] value() default "";
}

反射(自省)

概述

反射功能指在运行状态中

1.对于任意一个类,都能够 知道 这个类的所有的信息,如属性,方法,注解;

2.对于任意一个对象,都能够 调用 它的任意一个方法和属性;

即可以通过java代码,完全了解类,完全掌控对象,并且是在运行时动态地

动态体现 :下面代码执行起来后才知道要获取什么

String p=“com.chenshuyi.api.Apple”

Class clz = Class.forName§;

功能

获得泛型

动态代理

处理注解

等等基础都是:完全了解类,完全掌控对象,并且是在运行时。

优缺点

优:动态创建对象,很大的灵活,是框架灵魂

缺:速度慢,要通知jvm的。且有安全问题

反射API示例

1.java.lang.Class:

概述:编译完一个类之后,由jvm在堆内存的中产生了一个Class类的对象对应该编译的类(一个类只有一个Class对象),这个对象就包含了完整的类的信息。由对象得到类信息,这也是反射名称由来。

作用:Class类是反射的根源,用反射都要先获得其在堆内存的class对象,这永远是第一步!!!!!!!!!!!!!!!!!

还有一些类用于表示类的局部信息

  • java.lang.reflect.Method:代表一个类的方法
  • java.lang.reflect.Field:代表类的成员变量
  • java.lang.reflect.Constructor:代表类的构造器

2.反射API示例:

public class Apple {
    private int price;
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
    public static void main(String[] args) throws Exception{
        //1.返回指定类名name的Class对象
        Class clz = Class.forName("com.chenshuyi.api.Apple");
        
        //2.根据 Class 对象实例获取 Constructor 对象数组
        Constructor appleConstructor = clz.getConstructor();
        
        //3.使用 Constructor 对象的 newInstance 方法获取反射类的对象
        Object appleObj = appleConstructor.newInstance();
        
        //4.获取方法的 Method 对象
        Method setPriceMethod = clz.getMethod("setPrice", int.class);
        
        //5.利用 invoke 方法调用方法
        setPriceMethod.invoke(appleObj, 14);

    }
}

从代码中可以看到我们使用反射调用了 setPrice 方法,并传递了 14 的值。

API总结:

1.获取 Class 类对象有三种方法

//知道该类的全路径名时
Class.forName("java.lang.String");
//编译前就知道操作的 Class
Class clz = String.class;
//对象的 getClass() 方法
String str = new String("Hello");
Class clz = str.getClass();

2.反射调用某一个方法

1.获取方法的 Method 对象//参数为方法名称和参数类型
2.利用 invoke 方法调用方法,并传入参数//参数为对象和参数值

3.反射创建类对象

主要有两种方式:

通过 Class 对象的 newInstance() 方法

通过 Constructor 对象的 newInstance() 方法。

区别是:

通过 Constructor 对象创建类对象可以选择特定构造方法(类似new),而通过 Class 对象则只能使用默认的无参数构造方法。

4.通过反射获取类属性,方法、构造器

类属性:getFields和getDeclaredFields,前者不能获得private后者相反

方法,构造器:getMethod和getConstructor,同理要获得private,需要调用类似getDeclaredFields的方法

反射应用

jdbc

我们在使用 JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动程序;

spring中的bean对象

例如我们经常使用的 Spring 配置中,经常会有相关 Bean 的配置:



当我们在 XML 文件中配置了上面这段配置之后,Spring 便会在启动的时候利用反射去加载对应的 Apple 类。而当 Apple 类不存在或发生启发异常时,异常堆栈便会将异常指向调用的 invoke 方法。(invoke 方法的解析见Web基础笔记—动态代理)

反射操作注解:以一个迷你ORM为例

之前说反射能完全了解类,名不虚传,不仅属性和方法,注解也能获得

1.forname得到类的class对象 Class.forName(“test.Worker”);

2.class对象得到类的局部 Field name = worker_class.getDeclaredField(“name”);

3.局部对象获得注解类 LynField lynField = (LynField)name.getAnnotation(LynField.class);

(获得类上的注解就直接worker_class.getAnnotation()就行了,这里是要获得属性的注解)

4.调用注解类方法获得配置参数的值 lynField.colunmName()

public class demo {
     
    psvm {
     
        Class worker_class = Class.forName("test.Worker");
        Field name = worker_class.getDeclaredField("name");
        LynField lynField = (LynField)name.getAnnotation(LynField.class);
        System.out.println("colunmName = " + lynField.colunmName());//worker_name
        System.out.println("type = " + lynField.type());//varchar
        System.out.println("length = " + lynField.length());// 20 
        //根据获得bean对象的注解信息编写sql完成ORM
    }
}
//Object
@LynTable("db_worker")
class Worker {
     
    @LynField(colunmName = "worker_name",type = "varchar",length = 20)
    private String name;
}
//注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface LynField{
     
    String colunmName();   
    String type();    
    int length();
}

泛型(待深入研究)

Java中的泛型是什么 ? 使用泛型的好处是什么?

1.是什么:

泛型:把类型明确的工作推迟到创建对象或调用方法等情况下才进行

2.好处:

没有泛型:集合中存储对象都是Object需要在使用前进行类型转换,这样代码只有跑起来才能检查类型是否正确。

引入泛型:它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。

体现了泛型的健壮性,且省去了强制转换更加简洁。

泛型基础

1.类上定义泛型

类内部可以使用T指代一个类,具体哪个类由传入参数决定,这叫做参数化类型

public class ObjectTool {
   private T obj;

   public void setObj(T obj) {
     this.obj = obj;
   }
}

2.方法上定义泛型

方法内部可以使用T指代一个类,具体哪个类由传入参数决定,这叫做参数化类型

   public  void show(T t) {
     System.out.println(t);
   }

3.泛型只能是引用类型,因为泛型擦除后是作为Object而存在的,而基础数据类型并没有继承自Object,所以编译器不允许将基础类型声明为泛型类型。

类型通配符

看起来类似在方法上定义泛型,但是并不能够复用这个定义,实际作用是限制泛型的类型范围

一个需求:方法接收一个集合参数,遍历集合并把集合元素打印出来

用方法上定义泛型肯定可以,这里介绍使用类型通配符:

public void test(List list){
   for(int i=0;i

?号通配符表示可以匹配任意类型,任意的Java类都可以匹配。

注意:当我们使用?号通配符的时候:泛型对象是只读的。因为此时list的泛型类型仍然是不确定的,?只是起匹配作用,消除未确定类型的警告,不会在传入参数后把?确定为一个类型(和方法上定义泛型不同之处)。实际上直接List list也可以,只是会报没有确定集合类型的警告。

1.设定通配符上限

我想接收一个List集合,它只能操作数字类型的元素【Float、Integer、Double、Byte等数字类型都行】,怎么做?设定通配符上限

//只匹配Number的子类或自身,其他报错
List

2.设定通配符下限

//只匹配Type或Type的父类,其他报错

设定通配符的下限这并不少见,在TreeSet集合中就有….我们来看一下

    //对泛型类限制下限
    public TreeSet(Comparator comparator) {
        this(new TreeMap<>(comparator));
    }

这个构造函数的作用就是当我们创建一个TreeSet类型的变量的时候顺带传入一个Comparator,那么问题来了这个Comparator的选择是有很多的,但也不是都可以的,这里我们就用通配符来实现对这个类型的限制。实际上这就是通配符的作用:限制泛型的类型范围

它可以是Comparator,还可以是类型参数是String的父类,比如说Comparator(String传入可以向上转型,所以用这个比较器比较Stirng类是没问题的)

通配符和方法上定义泛型比较:通配符是方法上定义泛型的弱化

   //使用通配符
   public static void test(List list) {

   }

   //使用泛型方法
   public  void  test2(List t) {

   }

那么现在问题来了,我们使用通配符还是使用方法上定义泛型呢?

根据特点总结如下

1.对象需要修改/其他部分需要复用泛型的类型,使用方法上定义泛型,此时对象可修改,泛型可复用

2.反之使用通配符限制下类型范围就够了,所有能用类型通配符(?)解决的问题都能用泛型方法解决,通配符算是一个弱化的功能。

泛型擦除

编译器编译完带有泛形的java程序后,生成的class文件中将不再带有泛形信息(类型都变成Object类),这个过程称之为“擦除”。目的是和JDK5之前没有泛型的集合(如 List list1)兼容。

因为这一步,所以下面的代码才不会报错,

List list = new ArrayList<>();
//类型被擦除了,List没有泛型默认存Object
List list1 = list;

一个常见误区

你可以把List传递给一个接受List参数的方法吗?

不行!List和List毫无关系,想想这两者是继承关系吗?不是当然不能向上转型,这样只会让编译器报错。

异常

异常分类

首先,Throwable是 Java 语言中所有异常的超类。

下一层子类分为

1.Error:大多和编写者无关,不会抛出异常,是无法处理的(所以称为错误,而下面Exception可以处理的称为异常),例如内存耗尽,此时jvm一般选择线程终止。

2.Exception

有两个分支:

​ 1.RuntimeException:运行异常,一定是程序员问题,会被抛出异常

​ 2.CheckedException:检查异常,编译阶段的异常,Java编译器会强制要求你把这种异常进行try catch。是编译器认为程序总是会出错的区域,例如文件打开。

异常处理

1.抛出异常有三种形式,一是throw,一个throws,还有一种系统自动抛异常。

2.抛异常都是抛给调用者,只是throws是表示出现异常的一种可能性,出现了就抛出,throw用于抛出异常的动作,执行到这一定会抛异常。

3.try-catch

调用者如果知道怎么处理这个异常就会使用try catch捕捉并处理(处理常打印一些信息给用户,这就是我们使用框架时看到框架作者给我们信息的原理),如果上一级一直不处理,就会一直向上抛,最后会给jvm,jvm固定做法是:打印异常的跟踪栈消息,并终止程序。

4.finally方法

总是在return之前执行(不管是否有异常),当然也有可能不执行finally方法,都是比较极端情况,不用详记

  1. 在 finally 语句块第一行发生了异常
  2. 在前面的代码中用了System.exit()退出程序。
  3. 程序所在的线程死亡。
  4. 关闭CPU。

5.结合情景

我们调用方法时有时编译器会要求我们添加throws,不然报错,就是因为方法里面可能抛出异常。实际上我们加一个try catch块也不会报错了,只是编译器默认采用throws继续向上抛。

总而言之,方法内有异常,外面两种方案throws/catch。

多线程入门

1.线程、进程的基本概念。以及他们之间关系是什么?

进程是程序的一次执行过程,即一个进程就是一个执行中的程序,当程序在执行时,将会被操作系统载入内存中。

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间,而进程之间相反。但共享数据就会有临界区的出现。

2.java线程有哪些基本状态

6种状态,jvm的2和3被操作系统隐藏,我们只能看到 RUNNABLE 状态

1.线程创建之后它将处于 NEW(新建) 状态。

2.调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。

3.可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING(运行) 状态。

4.当线程执行 wait()方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。

5.而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。

6.当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。

7.线程在执行 完成run()方法之后将会进入到 TERMINATED(终止) 状态。

集合

四大接口和实现类概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mNtHtvni-1597201197985)(asset/20200523113946.png)]

List

List是有序的

1.ArrayList

2.Vector:

某一个时刻只能有一个线程访问vector

3.LinkList补充:

基于双向链表实现,只能顺序访问,可以当作栈,队列,双向队列使用,实现LinkList接口即可。

Set

1.HashSet补充

  • 同HashMap无序,同HashMap不是线程安全的

  • 它由HashMap支持,操作基本上都是直接调用底层 HashMap 的相关方法来完成

  • public HashSet() {
           
        map = new HashMap<>();
    }
    //默认情况下采用的是 initial capacity为16,load factor 为 0.75。
    
  • 对于 HashSet 中保存的对象,请注意正确重写其 equals 和 hashCode 方法,以保证放入的对象的唯一性,具体算法见java基础

    唯一和HashMap区别:不可重复,是无序集合

2.TreeSet补充

  1. TreeSet()是使用二叉树的原理对新add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序。
  2. Integer和String对象都可以进行默认的TreeSet排序,而自己定义的类必须实现Comparable接口,重写方法才可以正常使用。
  3. 对于HashSet是用Hash表来存储数据,而TreeSet是用红黑树来存储数据。 在不需要排序的时候,还是建议优先使用HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)
  4. 底层依赖依赖TreeMap(红黑树)

树结构的有序set

3.LinkedHashSet 补充

具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。

链表结构的有序hashset

Queue

  • LinkedList:可以用它来实现双向队列。
  • PriorityQueue:基于堆结构实现,可以用它来实现优先队列。

Map

HashMap

1.HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。

2.HashMap最多只允许一条记录的键为null,允许多条记录的值为null。

3.HashMap非线程安全,如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

TreeMap

基于红黑树实现。

HashTable

线程安全的HashMap,但是已经out,锁的太多效率差

ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。

LinkedHashMap

使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。其他和HashMap完全一样,只是多一个双向链表,

有序的HashMap,当然效率变低

源码分析

容器中的设计模式举例

1.迭代器模式

Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。

从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的对象。

List list = new ArrayList<>();
list.add("a");
list.add("b");
for (String item : list) {
    System.out.println(item);
}

2.适配器模式

java.util.Arrays的asList()方法 可以把数组类型转换为 List 类型。

List list = Arrays.asList(1, 2, 3);
@SafeVarargs
public static  List asList(T... a)
//参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。

HashMap(jdk1.7)

1. 存储结构

内部包含了一个 Node类型的数组 table。Node继承Entry 存储着键值对。它包含了四个字段,

static class Node<K,V> implements Map.Entry<K,V> {
          
    final int hash;// key对应的hash     
    final K key;//键      
    V value;//值            
    Node<K,V> next;// 指向下一个节点       

即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决hash冲突,同一个链表中存放hashcode相同的 Entry。

可能会有疑惑引入这个hash值有啥用?

后面我们可以看见计算key的hash值即可得到对应数组索引,这个操作是O(1)的复杂度,使得hashmap没有hash冲突下能O(1)查询,得到数组索引不就完成查询了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XcFtz8Gv-1597201197987)(https://camo.githubusercontent.com/5d81dca069e6bd97466ee40c53ea963696cfc1ef/68747470733a2f2f63732d6e6f7465732d313235363130393739362e636f732e61702d6775616e677a686f752e6d7971636c6f75642e636f6d2f696d6167652d32303139313230383233343934383230352e706e67)]

2. 拉链法的工作原理

HashMap map = new HashMap<>();
map.put("K1", "V1");
map.put("K2", "V2");
map.put("K3", "V3");
  • 新建一个 HashMap,默认大小为 16;
  • 插入 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。
  • 插入 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
  • 插入 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 前面。(注意到链表的插入是以头插法方式进行的)
  • 可见拉链法通过冲突时头插法插入对应桶的链表解决了hash冲突

3.查找的逻辑

查找需要分成两步进行:

  • 计算key所在的数组索引;
  • 若下标存在多个Node对象,在链表上顺序查找,看这些Node的key属性和要查找的相同否。时间复杂度显然和链表的长度成正比。即查询的复杂度可能是O(1)也可能是O(n),当然这里的n不是hashmap总元素个数
  • 易见HashMap中的链表出现越少,性能才会越好

学以致用:containsKey的复杂度是多少?

没有hash碰撞为O(1),有的话最差情况下(全部hash碰撞了)根据链表还是红黑树分别是O(n),O(logn)。

4.put 操作的逻辑

简单来说,就是先根据key计算hash后得到对应数组索引

1.没有hash碰撞时一个key对应一个索引,此时检查一下key是否存在,存在覆盖value,不存在插入value,一切都很自然。

2.有hash碰撞时我们会发现,key对应地数组下标已存在一个Entry 对象且该对象的key和插入的key不同,说明产生了hash碰撞。此时用拉链法解决,链表长度大于8用红黑树降低查找时间复杂度(n降低到logn)。

3.插入完毕后检查是否需要扩容

5. HashMap的重要属性和构造器

1.capacity

table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方(原因见求数组下标原理),也一定是。

2.int threshold;

threshold=capacity(容量)*loadFactory(负载因子)。

当 size 大于等于 threshold 就必须进行扩容操作

3.final float loadFactor;

负载因子,代表了数据的疏密程度的临界值,默认是0.75

加载因子存在的原因,还是因为减缓哈希冲突,越早扩容冲突越少,冲突越少链表越少,链表越少性能越好(见3.查找的逻辑)。当然太少会导致扩容次数过多。

所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容。

4.transient int modCount;(快速失败机制来源

非安全失败的集合都有的属性。保证并发安全性,但只在迭代器的迭代过程中有效

5.下面来看一个构造函数

如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值initialCapacity默认为16,loadFactory默认为0.75

public HashMap(int initialCapacity, float loadFactor) {
     //此处对传入的初始容量进行校验,最大不能超过MAXIMUM_CAPACITY = 1<<30(一个很大的int值)
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                            initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
     
        init();//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
    }

常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),而是在执行put操作的时候才真正构建table数组(1.7较早版本是初始化时即构建)

6.根据key计算得到hash值来确定数组索引详情

很多操作都需要确定数组下标

大概流程如下:

1.hash函数

hashcode的结果为32位,高16bit不变,低16bit和高16bit做异或。

目的是保证最终获取的存储位置尽量分布均匀。

2.hash函数的结果,通过indexFor进一步处理来获取实际的存储位置

    /**
     * 返回数组下标
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
//h&(length-1)保证获取的index一定在数组范围内,举个例子,默认容量16,length-1=15,h=18,转换成二进制计算为index=2。

疑问:我们一般都是通过取mod来保证获取的index一定在数组范围内,这里为什么能用&(length-1)?

因为当length=2的n次方幂时任何一个数&(length-1)等效modlength,这也是capacity 必须是2的n次方的原因

众所周知位运算比mod快多了,hashmap中大量使用了位运算。

3.hashcode算法:和地址有关,略

7.扩容-基本原理(1.8优化掉了rehash)

HashMap 采用动态扩容来根据当前的需要存储的键值对数量来调整 table 长度,原因讲loadFactor说了为了缓解hash冲突。

和扩容相关的参数主要有:capacity、size、threshold 和 load_factor,都在上一节做出了介绍。

扩容核心代码

if ((size >= threshold) && (null != table[bucketIndex])) {
  resize(2* table.length); 
  bucketIndex = indexFor(hash, table.length);
}

解析:

1.当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容

2.当需要扩容时,令 capacity 为原来的两倍

3.扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。

4.对新加入的键值对indexFor重新计算桶下标,即6.确定数组下标原理

**ps:1.8有优化,**不需要重新计算了,因为看6可见这个计算量还是很大的。

具体优化ToDo

8.JDK1.8中HashMap的存储结构优化

假如一个数组槽位上链上数据过多(即拉链过长的情况)导致性能下降该怎么办?
JDK1.8在JDK1.7的基础上针对增加了红黑树来进行优化。即当链表超过8时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能。

9.Hashmap的并发安全性问题

1.两个线程put到同一个位置,覆盖其中一个

2.多线程同时扩容时,只有一个线程能扩容成功,数组长度*2.其他线程数据put的数据丢失,并且扩容失败

如何实现线程安全的hashmap?:参考ConncurrentHashMap底层

10.红黑树

1.来历

来源于二叉查找树:左小右大,我们容易知道二叉查找树的查找效率是logn,优于链表的n。但是普通的二叉查找树在极端情况下可退化成链表,想象一下链表不就是特殊的一种二叉查找树吗?此时的增删查效率都会比较低下。为了避免这种情况,就出现了一些自平衡的查找树,比如 AVL(AVL树中任何节点的两个子树的高度最大差别为1),红黑树等。这些查找树通过定义一些性质,以达到自平衡。

自平衡:自动控制平衡

平衡:左右子树高度差控制在规定范围内

简单说:红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

2.红黑树的额外五个定义
  • 性质1:每个节点要么是黑色,要么是红色。
  • 性质2:根节点是黑色。
  • 性质3:每个叶子节点(NIL)是黑色。
  • 性质4:每个红色结点的两个子结点一定都是黑色。
  • 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

从性质5又可以推出:

  • 性质5.1:如果一个结点存在黑子结点,那么该结点肯定有两个子结点

有了上面的几个性质作为限制,即可避免二叉查找树退化成单链表的情况。但是,仅仅避免这种情况还不够,这里还要考虑某个节点到其每个叶子节点路径长度的问题。如果某些路径长度过长,那么,在对这些路径上的节点进行增删查操作时,效率也会大大降低。这个时候性质4和性质5用途就凸显了,有了这两个性质作为约束,即可保证任意节点到其每个叶子节点的路径最长不会超过最短路径的2倍

在Java中,叶子结点是为null的结点。下图是一个简单的红黑树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QZnkXwL6-1597201197991)(asset/webp)]

3.红黑树操作

树的操作无非查找、插入、删除。前面说到,红黑树是一种自平衡的二叉查找树,既然是二叉查找树的一种,那么查找过程和二叉查找树一样。而插入和删除操作就要复杂的多。尤其是删除操作,要处理的情况比较多。

旋转操作

在分析插入和删除操作前,这里需要插个队,先说明一下旋转操作,这个操作在后续操作中都会用得到。旋转操作分为左旋和右旋,左旋是将某个节点旋转为其右孩子的左孩子,而右旋是节点旋转为其左孩子的右孩子。

JavaSE知识点总结(持续更新,欢迎指正)_第1张图片

右旋节点 M 的步骤如下:

  1. 将节点 M 的左孩子引用指向节点 E 的右孩子(合并原右孩子
  2. 将节点 E 的右孩子引用指向节点 M,完成旋转(合并后旋转成E左孩子的右孩子

JavaSE知识点总结(持续更新,欢迎指正)_第2张图片

插入

TDhttps://segmentfault.com/a/1190000012728513

插入总结
删除
删除总结
红黑树和AVL比较

1.红黑树不要求完全平衡,AVL要求子树高度不超过1,而红黑树只要求性质5,降低了平衡的要求从而提高了性能

2.针对删除和插入后的失衡,红黑树只需要最多旋转3次复衡,复杂度为O(1)。这就是1所说的提高了性能,因为平衡要求没那么严苛自然平衡速度更快。但是由于平衡要求比较松,增删查的时间复杂度自然略高于AVL。但总的来说红黑树性能优于AVL。

3.由2可知两者的区别在于增删查和复衡效率偏向那个,故而可知两者的应用场景。搜索次数大大大于增删使用AVL,反之红黑树

考题

集合概述

1.List,Set,Map三者区别

  • List: 允许重复且有序
  • Set: 不允许重复且无序
  • Map(用Key来搜索的专家): 使用键值对存储,但Key不能重复

2.如何选用集合

1.需要根据键值获取到元素值时就选用Map接口下的集合:

​ 需要排序时选择TreeMap

​ 不需要排序时就选择HashMap

​ 需要保证线程安全就选用ConcurrentHashMap.

2.只需要存放元素值时,就选择实现Collection接口的集合

​ 需要保证元素唯一选择实现Set接口的集合比如TreeSet或HashSet

​ 不需要选择实现List接口的比如ArrayList或LinkedList

3.集合的线程安全性

我们常用的Arraylist,LinkedList,Hashmap基本都不是线程安全,解决办法很简单,java.util.concurrent包里面很多并发的容器(注意老的线程安全容器效率都很低基本废弃了,如vector。)

  1. ConcurrentHashMap: 可以看作是线程安全的 HashMap
  2. CopyOnWriteArrayList:可以看作是线程安全的 ArrayList,在读多写少的场合性能非常好,远远好于 Vector.
  3. ConcurrentLinkedQueue:高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
  4. BlockingQueue: 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
  5. ConcurrentSkipListMap :跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找。

List和iterator

Arraylist 与 LinkedList 区别?

都不是线程安全的

2.底层数据结构(key)

Arraylist 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。)

3.因为底层的数据结构,前者擅长查询,后者擅长修改。(数组修改要移动剩余元素,链表不要)数组修改的程序满足空间局部性,更快,其实复杂度都是O(n)

4.是否支持快速随机访问

即get(int index),只有Arraylist 支持,因为实现了RandomAccess

5.内存多余空间占用

ArrayList列表的结尾会预留一定的容量空间

LinkedList每一个元素都需要消耗比ArrayList更多的空间

RandomAccess接口

上面你说了RandomAccess 接口,说说这是什么?

实际上这个接口只是一个标识(方便一些方法用instanceof判断传入的list支不支持快速随机访问方法),就和序列化接口一样,里面没有实际的方法。支不支持快速随机访问还是由底层的数据结构决定。

标识到底怎么起作用?:instanceof

例如在 binarySearch() 方法中,它要判断传入的 list 是否 RamdomAccess 的实例,如果是,调用indexedBinarySearch()方法,如果不是,那么调用iteratorBinarySearch()方法

    public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
     
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

iterator

你上面提到iterator遍历,说说iterator吧

定义:迭代器其实设计模式中迭代器模式的实现,简单的说就是一个各种可迭代的类提供一个对外统一的迭代的接口,且不需要暴露类本身的细节。

至少包括两个方法:hasNext()–用于判断是否还有下一个,next()–用于取出下一个对象(remove–删除最近一次迭代出去的元素)。

好处:是即使这个类的存储数据结构临时发生改变(例如LinkedList变成了ArrayList,用for循环代码肯定要改变),不作任何修改迭代器的代码仍然可以正常工作。

note

1.迭代出的东西是原数组引用的拷贝,结果还是引用。那么如果集合中保存的元素是可变类型的,那么可以通过迭代出的元素修改原集合中的对象。

2.Iterator存储Person类对象的引用,不然是object类的引用,这点和集合同理。

3.foreach的底层是迭代器,foreach就是迭代器封装,更简洁一些,不要new类等等直接用关键字。

for循环/迭代器Iterator对比:

迭代器比for更安全,因为快速失败机制。

1.快速失败机制:

快速失败:Iterator在当前遍历的非安全失败的集合时,元素被更改的时,ModCount会+1,然后ModCount和expectedModCount会比较发现不相同,抛出 ConcurrentModificationException 异常进行提示,而for循环不行。这种机制避免了遍历时,多线程无意修改了集合元素问题和单线程代码错误修改了集合元素问题。所以说迭代器比for更安全

2.安全失败:非安全失败反义词,即形容集合不会抛出ConcurrentModificationException异常。原理是这种集合遍历时是拷贝原集合遍历,迭代器无法检测原集合,因为是从新集合new的迭代器。

3.当然如果你硬是想遍历时修改元素,可以使用Iterator的add和remove方法。这是不会报异常的,因为修改了和ModCount对比的expectedModCount属性,一起+1

ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?Vector为什么被废弃?

区别在于线程安全性,Vector类的所有方法都是同步的,只有一个线程时太慢了。

Vector被废弃的关键在于实现同步的方法太粗糙,单纯的把每个方法加上synchronized同步,效率太低。现在推出了很多优化的同步容器,例如CopyOnWriteArrayList,ConcurrentHashMap来代替。

说一说 ArrayList 的扩容机制吧

1.从构造方法说起:

   /**
     * 默认初始容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;
    
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    /**
     *默认构造函数,使用初始容量10构造一个空列表(无参数构造)
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。

2.add方法:

    /**
     * 将指定的元素追加到此列表的末尾。 
     */
    public boolean add(E e) {
   //添加元素之前,先调用ensureCapacityInternal方法
        ensureCapacityInternal(size + 1);  
        // Increments modCount!!
        //这里看到ArrayList添加元素的实质就相当于为数组赋值
        elementData[size++] = e;
        return true;
    }

3.add方法调用ensureCapacityInternal() 方法(确定容量方法)

 //得到最小扩容量
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
              // 获取默认的容量和传入参数的较大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

当 要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity =MAX(10,size+1)=10(直译是最小需求容量,即add后的新容量(初始=10,之后每次add+1))。

4.ensureCapacityInternal() 方法调用 ensureExplicitCapacity();

确定最大许可容量方法

  //判断是否需要扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //调用grow方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }

1.add 进第1个元素到 ArrayList 时调用此方法,明显minCapacity - elementData.length > 0,所以会进入 grow(minCapacity) 方法。

2.当add第2个元素时,minCapacity 为2,此时e lementData.length(容量)在添加第一个元素后扩容成 10 了(grow方法里扩容为10)。此时,minCapacity - elementData.length > 0不成立,所以不会进入 (执行)grow(minCapacity) 方法。不会扩容

3.直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大。进入grow方法进行扩容

5.扩容方法grow

   /**
     * 最大数组大小
     *
     /
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * ArrayList扩容的核心方法。
     */
    private void grow(int minCapacity) {
     
        // oldCapacity为旧容量,newCapacity为新容量
        int oldCapacity = elementData.length;
        //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
        //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
       // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
       //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 前面只是修改容量这个属性,这里实际为数组扩容,传参1.要复制的数组2.要复制的长度,明显参数2可以大于当前长度
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

扩容核心规则:

1.先计算新容量,int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity为偶数就是1.5倍,否则是1.5倍左右)。

2.新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,否则用计算的新容量当成数组新容量。(很符合直觉,计算的新容量还不够,哪我直接粗暴把最小需求满足,使得容量恰好足够)

举例

1.当add第1个元素时,oldCapacity 为0.所以新容量也为0,所以数组容量扩为10。

2.当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,所以数组容量扩为15。

实际扩容方法:原理就是复制并创建一个新长度的数组而已

// 前面只是修改容量这个属性,这里实际为数组扩容,传参1.要复制的数组2.要复制的长度。
elementData = Arrays.copyOf(elementData, newCapacity);

set

无序性

==hashset的无序!=随机,毕竟底层都是数组。==是指存储的数据在底层数组并非数组索引从小到大的顺序添加 ,而是根据数据的哈希值决定的存储在数组那个位置(类似hashmap原理),所以失去了顺序。

map

HashMap 和 HashSet 区别

HashSet 的源码非常非常少,因为除了 clone()等 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法

HashMap HashSet
实现了 Map 接口 实现 Set 接口
存储键值对 仅存储对象
调用 put()向 map 中添加元素 调用 add()方法向 Set 中添加元素
HashMap 使用键(Key)计算 Hashcode HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以 equals()方法用来判断对象的相等性,

HashMap 和 TreeMap 区别

TreeMapHashMap 都继承自AbstractMap ,所以大部分都相同。

TreeMap它还实现了

1.NavigableMap接口(有了对集合内元素的搜索的能力。)2.SortedMap`接口(有了对集合中的元素根据键排序的能力,默认是按 key 的升序排序,当然能通过外部/内部排序器Comparator更改规则)。

HashMap 和 Hashtable 的区别

1.HashTable 就是Arraylist的vector,也是因为同样原因被淘汰

2.前者null 作为键只能有一个,null 作为值可以有多个,后者不支持nul

l作键值

3.初始容量大小和每次扩充容量大小的不同

① HashMap 默认的初始化大小为 16。HashTable 不同

② 创建时如果给定了容量初始值, HashMap不会就用你的初始值,而是会将其扩充为 2 的幂次方大小。HashTable 不同

4.红黑树优化Hashtable 没有,因为根本不用了懒得不更新。

HashSet 如何检查重复

见基础的equal篇幅

HashMap 的长度为什么是 2 的幂次方

简单说:由hash值求数组下标时明显是需要一个取模操作,这是很耗时间的,所以用&(length-1)取代%length,这个取代成立的条件是length是2 的幂次方。详情见hashmap源码

多线程操作导致死循环问题

主要原因在于并发下的 Rehash(即length变换后,node肯定要重新hash定位,hash本身就是根据length求模得来,而且底层数组也是重新new一个) 会形成一个循环链表,查找链表时造成死循环。虽然已经在1.8修复,但并发还是不要HashMap 。

HashMap 有哪几种常见的遍历方式?

4 大类(迭代器、for、lambda、stream)遍历方式。除了 Stream 的并行循环(多线程),其他几种遍历方法的性能差别不大,但从简洁性和优雅性上来看,Lambda 和 Stream 无疑是最适合的遍历方式。除此之外我们还从「安全性」方面测试了 4 大类遍历结果,**从安全性来讲,我们应该使用,iterator.remove() 方法来进行删除,**这种方式是安全的在遍历中删除集合的方式。

ConcurrentHashMap

底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样

实现线程安全的方式(重要):

在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),然后给每一段(Segment)数据配一把锁。多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。

//Segment继承ReentrantLock(可重入锁),所以 Segment其实是扮演锁的角色,锁HashEntry 数组,HashEntry 数组才是存放k-v的数组。
static class Segment<K,V> extends ReentrantLock implements Serializable {
     
}

到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS(Compare and swap) 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是只是为了兼容旧版本;

1.synchronized 只锁定当前链表或红黑二叉树的首节点,即把锁的单位进一步缩小为节点。这样只要 hash 不冲突,就不会产生锁竞争,效率又提升 N 倍。

2.CAS怎么解决并发讲并发的时候再说

Collections 静态工具类

了解一下Collections 工具类常用方法:

1.排序

void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面

2.查找,替换操作

int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素。
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target).
boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素

3.同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)

你可能感兴趣的:(java,java)