Java基础面试题01(Java基础面试题,面向对象面试题,集合框架面试题)连载

前序

面试题繁多,为了避免单篇博客篇幅过长,这里分了一下列表,需要的请自取!!!

  1. Java基础面试题01(Java基础,面向对象,集合框架)
  2. Java基础面试题02(多线程面试题(JUC面试题),注解和反射面试题,异常面试题,网络编程面试题)
  3. Redis面试题,从基础到进阶,Redis常见面试题!Redis事务,持久化,过期策略,淘汰机制,读写模式,多线程,优化

部分内容参考:
javaguide
敖丙
重复的部分copy就不再造轮子了,有基于原来的代码做部分知识拓展,这里为了更加生动的描述出一个东西 加了很多笔者理解的白话部分!!!以及理论上的代码实现部分或者说实战部分,不搞那些虚的理论也是笔者自己成长的一个过程!!!

注:
大家好我是妈妈的好大儿,
笔者联系方式
QQ:3302254385
微信:yxc3302254385
交个朋友!
创作不易,三连十分感谢!!!

1.java基础

1.1 ==和equals的区别?

说明

  • 对于 byte,short,char,int,long,float,double,boolean 基础数据类型他们之间的比较,应用双等号(==),比较的是他们的值。
  • 对于引用类型(类、接口、数组) 比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false

equals方法说明

  • 没重写之前equals方法之前,底层也是比较的是地址值,但正常的情况下我们都会重写equals方法!
  • 比如String类,重写了equals方法,分析源码可以发现,
    • 1.他首先比较了地址值
    • 2.然后判断此对象是否为String类型
    • 3.将比较的对象强制转换成String类型,再转换成字符串数组
    • 4.将本字符串转换成字符串数组,逐一进行比较
public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

对于我们一般的对象进行比较,直接使用快捷生成equals方法即可,也就是比较每个属性值是否相等,以下面user为列!

public class User {
    String name;
    Integer age;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        if (name != null ? !name.equals(user.name) : user.name != null) return false;
        return age != null ? age.equals(user.age) : user.age == null;
    }

接着就要引出下面的问题------->1.1.2 为什么重写了Object类的equals(),还要重写hashCode()?

1.2 为什么重写了Object类的equals(),还要重写hashCode()?

分析Object类源码的equals方法

/**
* Note that it is generally necessary to override the {@code hashCode}
* method whenever this method is overridden, so as to maintain the
* general contract for the {@code hashCode} method, which states
* that equal objects must have equal hash codes.
*/
public boolean equals(Object obj) {
        return (this == obj);
}

由此可见默认比较的是地址值,源码中有注释说如果重写了此方法,都必须重写 hashCode方法,以便维护hashCode方法的常规协定,该协定规定相等的对象必须具有相等的哈希代码。

可是到了这一步我还是不太明白,为什么会有这个规范?我们来看一下hashcode方法的源码 ,截取了一部分注释

/**
 * Returns a hash code value for the object. This method is
 * supported for the benefit of hash tables such as those provided by
 * {@link java.util.HashMap}.
 *
 * As much as is reasonably practical, the hashCode method defined by
 * class {@code Object} does return distinct integers for distinct
 * objects. (This is typically implemented by converting the internal
 * address of the object into an integer, but this implementation
 * technique is not required by the
 * Java™ programming language.)
 */
public native int hashCode();

注释大概意思:
返回对象的哈希码值。 受益于散列表的支持,例如 {@link java.util.HashMap}提供的散列表。
在合理可行的范围内,由类{@code Object}定义的hashCode方法确实为不同的对象返回不同的整数。 (这通常是通过将对象的内部地址转换为整数来实现的,但是Java™编程语言不需要这种实现技术

摘抄:
Object类提供的默认实现确实保证每个对象的hash码不同(在对象的内存地址基础上经过特定算法返回一个hash码)。Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。 哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。初学者可以这样理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。

白话:
对于不同的对象他的hashcode值不一样,通过对象存储的物理地址计算得到!!!

1.3hashCode的作用

想要明白,必须要先知道Java中的集合。 总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set。
前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。
那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?

这就是Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。于是,Java采用了哈希表的原理。
这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。 如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;
如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。

先来试想一个场景,如果你想查找一个集合中是否包含某个对象,那么程序应该怎么写呢?通常的做法是逐一取出每个元素与要查找的对象一一比较,当发现两者进行equals比较结果相等时,则停止查找并返回true,否则,返回false。但是这个做法的一个缺点是当集合中的元素很多时,譬如有一万个元素,那么逐一的比较效率势必下降很快。于是有人发明了一种哈希算法来提高从该集合中查找元素的效率,这种方式将集合分成若干个存储区域(可以看成一个个桶),每个对象可以计算出一个哈希码,可以根据哈希码分组,每组分别对应某个存储区域,这样一个对象根据它的哈希码就可以分到不同的存储区域(不同的桶中)

image.png

实际的使用中,一个对象一般有key和value,可以根据key来计算它的hashCode。假设现在全部的对象都已经根据自己的hashCode值存储在不同的存储区域中了,那么现在查找某个对象(根据对象的key来查找),不需要遍历整个集合了,现在只需要计算要查找对象的key的hashCode,然后找到该hashCode对应的存储区域,在该存储区域中来查找就可以了,这样效率也就提升了很多。说了这么多相信你对hashCode的作用有了一定的了解,下面就来看看hashCode和equals的区别和联系。

在研究这个问题之前,首先说明一下JDK对equals(Object obj)和hashCode()这两个方法的定义和规范:在[Java](http://lib.csdn.net/base/javase)中任何一个对象都具备equals(Object obj)和hashCode()这两个方法,因为他们是在Object类中定义的。 equals(Object obj)方法用来判断两个对象是否“相同”,如果“相同”则返回true,否则返回false。 hashCode()方法返回一个int数,在Object类中的默认实现是“将该对象的内部地址转换成一个整数返回”。

白话
就是在我们使用 HashMap、HashSet和Hashtable,这些集合时我们要保证!key不能重复,也就是这两个对象不能相等,举个列子,我们创建了一个User类,重写了equals方法和hashcode方法!并创建而两个内容一致的对象!将这两个对象以key的形式存入到HashMap中!

/**
 * 创建
 * 自定义 测试equals方法 和 hashcode方法
 * @author cc
 * @date 2021-01-14-8:52
 */
@AllArgsConstructor
@NoArgsConstructor
public class User {
    String name;
    Integer age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (name != null ? !name.equals(user.name) : user.name != null) return false;
        return age != null ? age.equals(user.age) : user.age == null;
    }

    @Override
    public int hashCode() {
        System.out.println("调用了hashcod");
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (age != null ? age.hashCode() : 0);
        return result;
    }


}


//-------------------------------------------------------------------------------------
//测试 代码
    User user = new User("承承",111);
    User user2 = new User("承承",111);
    System.out.println("==  ->  "+(user==user2));//地址值  false 
    System.out.println("hashcode  ->  "+(user.hashCode()==user2.hashCode()));//hashcode  true
    System.out.println("equals  ->  "+ user.equals(user2));//对象内容  true

    Map<User,String> map = new HashMap<>();
    map.put(user,"user");
    map.put(user2,"user2");

    for (User key : map.keySet()) {
        System.out.println(map.get(key));
    }

测试结论:

  • 在我们没有重载hashcode方法时,2个对象虽然相等但是,2个对象都被存入到了map中,因为是通过地址值进行换算hashcode的hashcode不一样,就直接存入了
  • 重载hashcode方法后,只能存入一个对象,因为是通过属性计算hash值的!!! 二个对象的属性值相等!所以hashcode也相等,也就只能存入一个!
  • 在往map中put值时都调用了,该User对象的hashcode方法,底层源码还没分析,下次…
  • 正常的逻辑就是先比较对象的hashcode值是否相等,hashcode不相等那么对象它一定不相等!!!存入map
  • hashcode相等,可能存在hash冲突,再次调用equals方法进行内容比较!如过equals不相等就存入map

1.4String的特点?

1.不变性
不变性是指String对象一旦生成,则不能再对它进行改变。String的这个特性可以泛化成不变(immutable)模式,即一个对象的状态在对象被创建之后就不再发生变化。不变模式的主要作用在于,当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅提高系统性能。
注意:不变性可以提高多线程访问的性能。因为对象不可变,因此对于所有线程都是只读的,多线程访问时,即使不加同步也不会产生数据的不一致,故减小了系统开销。
由于不变性,一些看起来像是修改的操作,实际上都是依靠产生新的字符串实现的。比如String.substring()、String.concat()方法,它们都没有修改原始字符串,而是产生了一个新的字符串,这一点是非常值得注意的。如果需要一个可以修改的字符串,那么需要使用StringBuffer或者StringBuilder对象。

2.针对常量池的优化
针对常量池的优化指当两个String对象拥有相同的值时,它们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。

3.类的final定义
除以上两点外,final类型定义也是String对象的重要特点。作为final类的String对象在系统中不可能有任何子类,这是对系统安全性的保护。同时,在JDK 1.5版本之前的环境中,使用final定义有助于帮助虚拟机寻找机会,内联所有的final方法,从而提高系统效率。但这种优化方法在JDK 1.5以后,效果并不明显。

举列:
image.png
注意:
这里指的是字符串的内容不能改变,而不是引用不能改变。
String作为形式参数传递,因为String是一个不可改变的常量值所以可以把它看成基本类型的传递

1.5为什么java是值传递 ??

先看列子!!!

public static void main(String[] args) {
        //测试1 基本类型传递(数据在栈中 复制的是数据的值),所以函数内部对参数进行操作不会对外部变量产生影响
        int a =10;
        int b=20;
        test1(a,b);
        System.out.println("原本");
        System.out.println("a--->"+a);
        System.out.println("b--->"+b);
        /*
        方法
        a--->20
        b--->10
        
        原本
        a--->10
        b--->20
		*/

        public static void test1(int a,int b){
        int temp=a;
        a=b;
        b=temp;
        System.out.println("方法");
        System.out.println("a--->"+a);
        System.out.println("b--->"+b);
    }
	
    
    	
    
        //测试2 对象类型传递 (new 出来的对象在堆内存中 对应一个地址值  传递的是这个对象的地址值),因为传递的是地址的拷贝所以函数内对值的操作对外部变量是可见的。
        User user = new User().setUserName("承承");
        test2(user);
        System.out.println("原本");
        System.out.println(user);
    
        public static void test2(User user){
        user.setUserName("修改承承");//通过值传递的地址值修改了
        System.out.println("方法");
        System.out.println(user);
    }
	        /*
方法
User(userId=null, userMatchId=null, userName=修改承承, userPhone=null, userSex=null, userOpenId=null, userType=null, createTime=null, updateTime=null, userPassword=null, userInfo=null, userMotto=null, userWechatSession=null, userProvince=null, userCity=null, userDistrict=null, userBirthday=null)

原本
User(userId=null, userMatchId=null, userName=修改承承, userPhone=null, userSex=null, userOpenId=null, userType=null, createTime=null, updateTime=null, userPassword=null, userInfo=null, userMotto=null, userWechatSession=null, userProvince=null, userCity=null, userDistrict=null, userBirthday=null)
		*/
    
    
        //测试3 String对象类型传递  (new 出来的对象在堆内存中 对应一个地址值  传递的是这个对象的地址值),因为String是一个常量是不可变的字符串 所以导致这里开辟了新的堆内存空间 不影响原来的对象
        String s=new String("韬韬");
        test3(s);
        System.out.println("原本");
        System.out.println(s);
		/*
		方法
        修改韬韬
        
        原本
        韬韬
        */
    
    public static void test3(String s){
        s="修改韬韬";//因为String是一个常量是不可变的字符串 所以导致这里开辟了新的堆内存空间 不影响原来的对象 ,虽然复制了对象的地址值  这里相当于 s = new String("修改韬韬");
        System.out.println("方法");
        System.out.println(s);
    }

    
    
        //测试4 对象类型传递  (传递的是对象的地址值,但是在方法内部创建了 新的对象)
        User user2 = new User().setUserName("承承2");
        test4(user2);
        System.out.println("原本");
        System.out.println(user2);
    
      public static void test4(User user){
        user = new User();//这里开辟了新的空间
        user.setUserName("修改承承2");//通过值传递的地址值修改了
        System.out.println("方法");
        System.out.println(user);
    }
		/*
方法
User(userId=null, userMatchId=null, userName=修改承承2, userPhone=null, userSex=null, userOpenId=null, userType=null, createTime=null, updateTime=null, userPassword=null, userInfo=null, userMotto=null, userWechatSession=null, userProvince=null, userCity=null, userDistrict=null, userBirthday=null)

原本
User(userId=null, userMatchId=null, userName=承承2, userPhone=null, userSex=null, userOpenId=null, userType=null, createTime=null, updateTime=null, userPassword=null, userInfo=null, userMotto=null, userWechatSession=null, userProvince=null, userCity=null, userDistrict=null, userBirthday=null)

        */
    }




结论:
首先明确两点

  • java中不管是值对象还是引用对象都是值传递
  • 在其他方法里面改变引用类型的值肯定是通过引用改变的,当传递引用对象的时候传递的是复制过的对象句柄(引用),注意这个引用是复制过的,也就是说又在内存中复制了一份句柄,这时候有两个句柄是指向同一个对象的,所以你改变这个句柄对应空间的数据会影响外部的变量的
  • 虽然是复制的但是引用指向的是同一个地址,当你把这个句柄指向其他对象的引用时并不会改变原对象,因为你拿到的句柄是复制过的引用。总结java中的句柄(引用)是复制过的,所以说java只有值传递

也就是说:

  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
  • 一个方法可以改变一个对象参数的状态。
  • 一个方法不能让对象参数引用一个新的对象。

1.6Java 语言有哪些特点?

  1. 简单易学;
  2. 面向对象(封装,继承,多态);
  3. 平台无关性( Java 虚拟机实现平台无关性);
  4. 可靠性;
  5. 安全性;
  6. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);
  7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便);
  8. 编译与解释并存;

修正(参见: issue#544):C11 开始(2011 年的时候),C就引入了多线程库,在 windows、linux、macos 都可以使用std::threadstd::async来创建线程。参考链接:http://www.cplusplus.com/reference/thread/thread/?kw=thread

1.7关于 JVM JDK 和 JRE 最详细通俗的解答

JVM
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。

什么是字节码?采用字节码的好处是什么?
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

Java 程序从源代码到运行一般有下面 3步:
image.png
我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。

HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。

总结:

Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

JDK 和 JRE
**JDK **
是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。

JRE
是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。
如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。

1.8 Java 和 C++的区别?

我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过 C++,也要记下来!

  • 都是面向对象的语言,都支持封装、继承和多态
  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
  • Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存
  • 在 C 语言中,字符串或字符数组最后都会有一个额外的字符’\0’来表示结束。但是,Java 语言中没有结束符这一概念。 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189](

1.9 为什么说 Java 语言“编译与解释并存”?

高级编程语言按照程序的执行方式分为编译型和解释型两种。
简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;

解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。比如,你想阅读一本英文名著,你可以找一个英文翻译人员帮助你阅读,有两种选择方式,你可以先等翻译人员将全本的英文名著(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。
Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(*.class 文件),这种字节码必须由 Java 解释器来解释执行。因此,我们可以认为 Java 语言编译与解释并存。

1.10 Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符?

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。 更多关于类型擦除的问题,可以查看这篇文章:《Java 泛型类型擦除以及类型擦除带来的问题》

List<Integer> list = new ArrayList<>();

list.add(12);
//这里直接添加会报错
list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//但是通过反射添加,是可以的
add.invoke(list, "kl");

System.out.println(list)

泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
1.泛型类:

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{

    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}

如何实例化泛型类:

Generic<Integer> genericInteger = new Generic<Integer>(123456);

2.泛型接口

public interface Generator<T> {
    public T method();
}

实现泛型接口,不指定类型

class GeneratorImpl<T> implements Generator<T>{
    @Override
    public T method() {
        return null;
    }
}

实现泛型接口,指定类型:

class GeneratorImpl<T> implements Generator<String>{
    @Override
    public String method() {
        return "hello";
    }
}

3.泛型方法

  public static < E > void printArray( E[] inputArray )
   {
         for ( E element : inputArray ){
            System.out.printf( "%s ", element );
         }
         System.out.println();
    }

使用:

// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray  );
printArray( stringArray  );

常用的通配符为: T,E,K,V,?

  • ? 表示不确定的 java 类型
  • T (type) 表示具体的一个 java 类型
  • K V (key value) 分别代表 java 键值中的 Key Value
  • E (element) 代表 Element

1.11重载和重写

重载:
发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
下面是《Java 核心技术》对重载这个概念的介绍:
image.png
综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。

重写:
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。

  1. 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
  2. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
  3. 构造方法无法被重写

综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变

区别点重载方法重写方法
发生范围同一个类子类
参数列表必须修改一定不能修改
返回类型可修改子类方法返回值类型应比父类方法返回值类型更小或相等
异常可修改子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
访问修饰符可修改一定不能做更严格的限制(可以降低限制)
发生阶段编译期运行期

方法的重写要遵循“两同两小一大”(以下内容摘录自《疯狂 Java 讲义》,issue#892 ):

  • “两同”即方法名相同、形参列表相同;
  • “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
  • “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。

⭐️ 关于 重写的返回值类型 这里需要额外多说明一下,上面的表述不太清晰准确:如果方法的返回类型是void和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。

光听理论是没有什么作用的!!其实很多小白包括自己初期也是不太能掌握住精髓!!!下面以一个案列体会出重写的好处!!!(深刻体会到,接口,抽象类,实现类的实战代码)

1.先定义抽象接口:

/**
 * @Author: Joker-CC
 * @Date 2021/09/03 10:13
 * @Description:  任务接口
 * @Version: 1.0
 */
public interface TaskInterface {

    /**
     * 执行任务的逻辑
     * @return
     */
   String handleTask();

    /**
     * 获取任务请求地址
     * @return
     */
    String getTaskSendReslutUrl();

    /**
     * 发送任务执行的结果
     * @return
     */
   void sendTaskData();

}

** 2.定义抽象类**

/**
 * @Author: Joker-CC
 * @Date 2021/07/27 10:13
 * @Description:
 * @Version: 1.0
 */
public abstract class TaskAbstract implements TaskInterface{

    /**
     * 实现接口--->再变成抽象方法--->子类自定义实现--->抽象父类统一调用
     * @return
     */
    @Override
    public abstract String getTaskSendReslutUrl();

    /**
     * 实现接口--->再变成抽象方法--->子类自定义实现--->抽象父类统一调用
     * @return
     */
    @Override
    public abstract String handleTask();

	/**
     * 父类实现接口--->调用抽象方法的子类实现
     */
    @Override
    public void sendTaskData() {
        //子类实现执行任务,并获取结果
        String taskResult = handleTask();

        //子类实现任务结果发送地址,并获取结果
        String taskSendReslutUrl = getTaskSendReslutUrl();

        System.out.println("推送数据到第三方接口:接口地址--->"+taskSendReslutUrl+"推送数据--->"+taskResult);
    }


}

3.子类实现A

/**
 * @Author: Joker-CC
 * @Date 2021/07/27 10:16
 * @Description: 自定义任务处理A
 * @Version: 1.0
 */
public class TaskHandleA extends TaskAbstract {

    @Override
    public String getTaskSendReslutUrl() {
        return "任务A地址";
    }

    @Override
    public String handleTask() {
        //业务逻辑
        return "任务A处理结果";
    }
}

4.子类实现B

/**
 * @Author: Joker-CC
 * @Date 2021/07/27 10:16
 * @Description:    自定义任务处理B
 * @Version: 1.0
 */
public class TaskHandleB extends TaskAbstract {


    @Override
    public String getTaskSendReslutUrl() {
        return "任务B地址";
    }

    @Override
    public String handleTask() {
        return "任务B处理结果";
    }
}

5.运行方法

    public static void main(String[] args) {
        TaskAbstract taskAbstractA = new TaskHandleA();
        taskAbstractA.sendTaskData();

        System.out.println("\n-------------------------------------------------------\n");

        TaskAbstract taskAbstractB = new TaskHandleB();
        taskAbstractB.sendTaskData();

    }

6.运行结果
image.png

总结:
显而易见看到,通过接口,抽象类的规范我们可以把不同的部分,抽取出来作为抽象方法!!由子类自定义实现!
把相同的部分,由抽象类去实现,调用子类的具体实现,避免重复代码!!!
十分优雅!易拓展!

1.12深拷贝 vs 浅拷贝

  1. 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
  2. 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝

image.png

1.13自动装箱与拆箱

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

更多内容见:[深入剖析 Java 中的装箱和拆箱]https://www.cnblogs.com/dolphin0520/p/3780005.html

1.14String StringBuffer 和 StringBuilder 的区别是什么?

可变性
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以String 对象是不可变的。

补充(来自issue 675):
在 Java 9 之后,String 、StringBuilder 与 StringBuffer 的实现改用 byte 数组存储字符串 private final byte[] value

而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是AbstractStringBuilder 实现的,大家可以自行查阅源码。
AbstractStringBuilder.java

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;

    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }}

线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操作少量的数据: 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

1.15Object 类的常见方法总结

Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:

public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。

public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。

protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。

public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。

public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。

public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。

public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。

public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。

public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念

protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作

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

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

1.7String为什么是不可变的??

可以看看—>https://www.zhihu.com/question/31345592
Java自出生那天起就是"为人民服务”·这也就是为什么Java做不了病毒,也不一定非得是病毒。反正总之就是为了安全,人家Java的开发者目的就是不想让Java干这类危险的事儿**,Java并不是操作系统本地语言,换句话说Java必须借助操作系统本身的力里才能做事,JDK中提供的好多核心类比如string 这类的类的内部好多方法的实现都不是Java编程语言本身编写的,好多方法都是调用的操作系统本地的API,这就是著名的"本地方法调用”·也只有这样才能做事。这种类是非常底层的。**和操作系统交流频算的,那么如果这种类可以被继承的话,如果我们再把它的方法重写了,往操作系统内部写入一段具有恶意攻击性质的代码什么的,这不就成了核心病毒了么?

首先String类是用final关键字修饰,这说明String不可继承。再看下面,String类的主力成员字段value是个char[ ]数组,而且是用final修饰的。final修饰的字段创建以后就不可改变。

虽然String内存地址不可变,但是里面存储的字符数组是可以改变的
**为了安全,比如我们用String做参数传递时,方法内修改了String的值,如果String是可变的就会导致,我们真正的主体受到影响,导致一些意外的情况发生,StringBuffer或StringBuild就会产生这种问题!!!**可以去看一下1.5为什么java是值传递

还有一个大家都知道,就是在并发场景下,多个线程同时读一个资源,是不会引发竟态条件的。只有对资源做写操作才有危险。不可变对象不能被写,所以线程安全
因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串
总结:
1.java是基础操作系统的方法调用实现String类的,是大部分哈,这种底层的东西如果能被继承修改会造成很大的安全隐患
2.避免参数传递中一些不可避免的错误
3.避免多线程修改的问题
4.做为map的key,String的hashCode能被快速查找 不需要重新计算!!!

1.18 接口和抽象方法的区别??

1、基本语法区别
Java中接口和抽象类的定义语法分别为interface与abstract关键字。

抽象类
在Java中被abstract关键字修饰的类称为抽象类,被abstract关键字修饰的方法称为抽象方法,抽象方法只有方法的声明,没有方法体。抽象类的特点:
a、抽象类不能被实例化只能被继承;
b、包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;
c、抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;
d、一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;
e、抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。

**接口:**Java中接口使用interface关键字修饰,特点为:
a、接口可以包含变量、方法;变量被隐士指定为public static final,方法被隐士指定为public abstract(JDK1.8之前);
b、接口支持多继承,即一个接口可以extends多个接口,间接的解决了Java中类的单继承问题;
c、一个类可以实现多个接口;
d、JDK1.8中对接口增加了新的特性:(1)、默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;(2)、静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名)。

2、面试题:接口与抽象类的区别
相同点
(1)都不能被实例化 (2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。

不同点
(1)接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
(3)接口强调特定功能的实现,而抽象类强调所属关系。
(4)接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。

2.JAVA面向对象

2.1 面向对象和面向过程的区别

  • 面向过程面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。
  • 面向对象面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低

参见 issue : 面向过程 :面向过程性能比面向对象高??

这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机械码。
而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。

2.2. 构造器 Constructor 是否可被 override?

Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。

2.3. 在 Java 中定义一个不做事且没有参数的构造方法的作用

Java 程序在执行子类的构造方法之前,如果没有用 super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。

2.4. 成员变量与局部变量的区别有哪些?

  1. 从语法形式上看:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  2. 从变量在内存中的存储方式来看:如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

2.5. 创建一个对象用什么运算符?对象实体与对象引用有何不同?

new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。

2.6. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?

主要作用是完成对类对象的初始化工作。

可以执行。

因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,这时候,就不能直接 new 一个对象而不传递参数了,所以我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。

2.7. 构造方法有哪些特性?

  1. 名字与类名相同。
  2. 没有返回值,但不能用 void 声明构造函数。
  3. 生成类的对象时自动执行,无需调用。

2.8封装

封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。
如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了(当然现在还有很多其他方法 ,这里只是为了举例子)。

2.9继承

不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。

关于继承如下 3 点请记住:

  1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。(以后介绍)。

2.10. 多态

多态,顾名思义,表示一个对象具有多种的状态。具体表现为父类的引用指向子类的实例。
多态的特点:

  • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
  • 多态不能调用“只在子类存在但在父类不存在”的方法;
  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

3.java集合

3.1Java 集合概览

从下图可以看出,在 Java 中除了以 Map 结尾的类之外, 其他类都实现了 Collection 接口。
并且,以 Map 结尾的类都实现了 Map 接口。
image.png

image.png

List
  • ArraylistObject[]数组
  • VectorObject[]数组
  • LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
Set
  • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
  • LinkedHashSetLinkedHashSetHashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)

再来看看 Map 接口下面的集合。

Map
  • HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
  • LinkedHashMapLinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》
  • Hashtable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap: 红黑树(自平衡的排序二叉树)

3.2为什么要使用集合?

当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端,因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。
数组的缺点是一旦声明之后,长度就不可变了;
同时,声明数组时的数据类型也决定了该数组存储的数据的类型;
而且,数组存储的数据是有序的、可重复的,特点单一。
但是集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据。

3.3 集合基本API

先来看最上层的 Collection.
image.png
Collection 里还定义了很多方法,这些方法也都会继承到各个子接口和实现类里,而这些 API 的使用也是日常工作和面试常见常考的,所以我们先来看下这些方法。
操作集合,无非就是「增删改查」四大类,也叫 CRUD:

Create, Read, Update, and Delete.
那我也把这些 API 分为这四大类:

功能方法
add()/addAll()
remove()/ removeAll()
Collection Interface 里没有
contains()/ containsAll()
其他isEmpty()/size()/toArray()

下面具体来看:
增:
boolean add(E e);

add() 方法传入的数据类型必须是 Object,所以当写入基本数据类型的时候,会做自动装箱 auto-boxing 和自动拆箱 unboxing。
还有另外一个方法 addAll(),可以把另一个集合里的元素加到此集合中。
boolean addAll(Collection<? extends E> c);

删:
boolean remove(Object o);

remove()是删除的指定元素。
那和 addAll() 对应的,
自然就有removeAll(),就是把集合 B 中的所有元素都删掉。
boolean removeAll(Collection<?> c);

改:
Collection Interface 里并没有直接改元素的操作,反正删和增就可以完成改了嘛!

查:

  • 查下集合中有没有某个特定的元素:

boolean contains(Object o);

  • 查集合 A 是否包含了集合 B:

boolean containsAll(Collection<?> c);

还有一些对集合整体的操作:

  • 判断集合是否为空:

boolean isEmpty();

  • 集合的大小:

int size();

  • 把集合转成数组:

Object[] toArray();

以上就是 Collection 中常用的 API 了。
在接口里都定义好了,子类不要也得要。
当然子类也会做一些自己的实现,这样就有了不同的数据结构。
那我们一个个来看。

3.4List接口以及子类

image.png
List 最大的特点就是:
有序,可重复。

这一下把 Set 的特点也说出来了,和 List 完全相反,Set 是 无序,不重复的。

List 的实现方式有 LinkedList 和 ArrayList 两种,那面试时最常问的就是这两个数据结构如何选择。
对于这类选择问题:
一是考虑数据结构是否能完成需要的功能
如果都能完成,二是考虑哪种更高效
万事都是如此啊。
那具体来看这两个 classes 的 API 和它们的时间复杂度:

功能方法ArrayListLinkedList
add(E e)O(1)O(1)
add(int index, E e)O(n)O(n)
remove(int index)O(n)O(n)
remove(E e)O(n)O(n)
set(int index, E e)O(1)O(n)
get(int index)O(1)O(n)

稍微解释几个:
add(E e) 是在尾巴上加元素,虽然 ArrayList 可能会有扩容的情况出现,但是均摊复杂度(amortized time complexity)还是 O(1) 的。
add(int index, E e)是在特定的位置上加元素,LinkedList 需要先找到这个位置,再加上这个元素,虽然单纯的「加」这个动作是 O(1) 的,但是要找到这个位置还是 O(n) 的。(这个有的人就认为是 O(1),和面试官解释清楚就行了,拒绝扛精。
remove(int index)是 remove 这个 index 上的元素,所以

  • ArrayList 找到这个元素的过程是 O(1),但是 remove 之后,后续元素都要往前移动一位,所以均摊复杂度是 O(n);
  • LinkedList 也是要先找到这个 index,这个过程是 O(n) 的,所以整体也是 O(n)。

remove(E e)是 remove 见到的第一个这个元素,那么

  • ArrayList 要先找到这个元素,这个过程是 O(n),然后移除后还要往前移一位,这个更是 O(n),总的还是 O(n);
  • LinkedList 也是要先找,这个过程是 O(n),然后移走,这个过程是 O(1),总的是 O(n).

那造成时间复杂度的区别的原因是什么呢?

  • 因为 ArrayList 是用数组来实现的。
  • 而数组和链表的最大区别就是数组是可以随机访问的(random access)

这个特点造成了在数组里可以通过下标用 O(1) 的时间拿到任何位置的数,而链表则做不到,只能从头开始逐个遍历。
也就是说在「改查」这两个功能上,因为数组能够随机访问,所以 ArrayList 的效率高。

那「增删」呢?
如果不考虑找到这个元素的时间,
数组因为物理上的连续性,当要增删元素时,在尾部还好,但是其他地方就会导致后续元素都要移动,所以效率较低;而链表则可以轻松的断开和下一个元素的连接,直接插入新元素或者移除旧元素。
但是呢,实际上你不能不考虑找到元素的时间啊。。。而且如果是在尾部操作,数据量大时 ArrayList 会更快的。

  1. 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全;
  2. 底层数据结构: Arraylist 底层使用的是 **Object**** 数组**;LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
  3. 插入和删除是否受元素位置的影响:**ArrayList**** 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **LinkedList**** 采用链表存储,所以对于****add(E e)**方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置**i**插入和删除元素的话(**(add(int index, E element)**) 时间复杂度近似为**o(n))**因为需要先移动到指定位置再插入。
  4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  5. 内存空间占用: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

论遍历ArrayList要比LinkedList快得多,ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销。

3.5Arraylist 和 Vector 的区别?

  • ArrayListList 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ;(JDK1.2)
  • VectorList 的古老实现类,底层使用Object[ ] 存储,线程安全的。(JDK1.0)

区别:
一.线程安全问题;
二.是扩容时扩多少的区别。

这个得看看源码:
image.png
这是 ArrayList 的扩容实现,这个算术右移操作是把这个数的二进制往右移动一位,最左边补符号位,但是因为容量没有负数,所以还是补 0.
那右移一位的效果就是除以 2,那么定义的新容量就是原容量的 1.5 倍

再来看 Vector 的:
image.png
因为通常 capacityIncrement 我们并不定义,所以默认情况下它是扩容两倍

3.6 补充内容:双向链表和双向循环链表

双向链表: 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。
另外推荐一篇把双向链表讲清楚的文章:https://juejin.im/post/5b5d1a9af265da0f47352f14

双向循环链表: 最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环。

3.7 补充内容:RandomAccess 接口

 public interface RandomAccess {
 }

查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
在 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);
     }

ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢?我觉得还是和底层数据结构有关!ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!

3.8 补充内容 ArrayList源码分析

构造:

ArrayList可以通过构造方法在初始化的时候指定底层数组的大小。
通过无参构造方法的方式ArrayList()初始化,则赋值底层数Object[] elementData为一个默认空数组Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}所以数组容量为0,只有真正对数据进行添加add时,才分配默认DEFAULT_CAPACITY = 10的初始容量。
大家可以分别看下他的无参构造器和有参构造器,无参就是默认大小,有参会判断参数。
image.png

add方法(目前只关心扩容):

      /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size; 

	/**
     * Inserts the specified element at the specified position in this
     * list. Shifts the element currently at that position (if any) and
     * any subsequent elements to the right (adds one to their indices).
     *
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        //范围检查添加
        rangeCheckForAdd(index);
		
        //确保内部容量方法
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

点击ensureCapacityInternal


 	/**
     * 确保内部容量
     * @param minCapacity  最小容量--->
     * 刚刚传递的参数  private int size;   
     *  minCapacity=size+1=0+1=1
     * 所以最小容量为1 
     */
private void ensureCapacityInternal(int minCapacity) {
    	//确保显式容量(计算容量(elementData,minCapacity))
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

点击calculateCapacity

   /**
     * Default initial capacity.
     * 默认容量
     */
    private static final int DEFAULT_CAPACITY = 10;	

/**
     * 计算容量
     */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    	//如果为空数组,取默认容量和最小容量去比较最大值  取最大值
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
 			
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

扩容:

其实实现方式比较简单,他就是通过数组扩容的方式去实现的。
就比如我们现在有一个长度为10的数组,现在我们要新增一个元素,发现已经满了,那ArrayList会怎么做呢?
image.png
第一步
他会重新定义一个长度为10+10/2的数组也就是新增一个长度为15的数组,扩容1.5倍
image.png
第二步:
然后把原数组的数据,原封不动的复制到新数组中,这个时候再把指向原数的地址换到新数组,ArrayList就这样完成了一次改头换面
image.png

插入:

   /**
     * Appends the specified element to the end of this list.
     * 将指定的元素附加到此列表的末尾
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        //确保内部容量方法
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

点击ensureCapacityInternal


 	/**
     * 确保内部容量
     * @param minCapacity  最小容量--->
     * 刚刚传递的参数  private int size;   
     *  minCapacity=size+1=0+1=1
     * 所以最小容量为1 
     */
private void ensureCapacityInternal(int minCapacity) {
    	//确保显式容量(计算容量(elementData,minCapacity))
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

点击calculateCapacity

   /**
     * Default initial capacity.
     * 默认容量
     */
    private static final int DEFAULT_CAPACITY = 10;	

/**
     * 计算容量
     */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    	//如果为空数组,取默认容量和最小容量去比较最大值  取最大值
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
 			
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

返回十一层调用链查看,点击ensureExplicitCapacity

 /**
 *确保显式容量
 *@Param  minCapacity  就是我计算的容量
 */
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
		
        // overflow-conscious code(发现容量不足)扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

再点击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);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

不知道大家看懂arraycopy的代码没有,我画个图解释下,你可能就明白一点
比如有下面这样一个数组我需要在index 5的位置去新增一个元素A
image.png

那从代码里面我们可以看到,他复制了一个数组,是从index 5的位置开始的,然后把它放在了index 5+1的位置
image.png
给我们要新增的元素腾出了位置,然后在index的位置放入元素A就完成了新增的操作了
image.png
至于为啥说他效率低,我想我不说你也应该知道了,我这只是在一个这么小的List里面操作,要是我去一个几百几千几万大小的List新增一个元素,那就需要后面所有的元素都复制,然后如果再涉及到扩容啥的就更慢了不是嘛。

ArrayList(int initialCapacity)会不会初始化数组大小?

不会初始化数组大小!
而且将构造函数与initialCapacity结合使用,然后使用set()会抛出异常,尽管该数组已创建,但是大小设置不正确。
使用sureCapacity()也不起作用,因为它基于elementData数组而不是大小。
还有其他副作用,这是因为带有sureCapacity()的静态DEFAULT_CAPACITY。
进行此工作的唯一方法是在使用构造函数后,根据需要使用add()多次。
大家可能有点懵,我直接操作一下代码,大家会发现我们虽然对ArrayList设置了初始大小,但是我们打印List大小的时候还是0,我们操作下标set值的时候也会报错,数组下标越界。
再结合源码,大家仔细品读一下,这是Java Bug里面的一个经典问题了,还是很有意思的,大家平时可能也不会注意这个点。
image.png

删除:

ArrayList插入删除一定慢么?
取决于你删除的元素离数组末端有多远,ArrayList拿来作为堆栈来用还是挺合适的,push和pop操作完全不涉及数据移动操作。

那他的删除怎么实现的呢?
删除其实跟新增是一样的,不过叫是叫删除,但是在代码里面我们发现,他还是在copy一个数组。

为啥是copy数组呢?
image.png
继续打个比方,我们现在要删除下面这个数组中的index5这个位置
image.png
那代码他就复制一个index5+1开始到最后的数组,然后把它放到index开始的位置
image.png
index5的位置就成功被”删除“了其实就是被覆盖了,给了你被删除的感觉。
同理他的效率也低,因为数组如果很大的话,一样需要复制和移动的位置就大了。

其他也可以看看这个:
https://mp.weixin.qq.com/s?__biz=MzAwNDA2OTM1Ng==&mid=2453142021&idx=2&sn=506a47e86fe0cd74d0eb52b56a0c831d&scene=21#wechat_redirect

  1. 9Set以及子类接口
    最后一个 Set,刚才已经说过了 Set 的特定是无序,不重复的。
    就和数学里学的「集合」的概念一致。
    image.png
    Set 的常用实现类有三个:
    HashSet: 采用 Hashmap 的 key 来储存元素,主要特点是无序的,基本操作都是 O(1) 的时间复杂度,很快。
    看一下源码:
    创建一个HashSet
   /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }

往HashSet添加元素

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

/**
     * Adds the specified element to this set if it is not already present.
     * More formally, adds the specified element <tt>e</tt> to this set if
     * this set contains no element <tt>e2</tt> such that
     * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
     * If this set already contains the element, the call leaves the set
     * unchanged and returns <tt>false</tt>.
     *
     * @param e element to be added to this set
     * @return <tt>true</tt> if this set did not already contain the specified
     * element
     */
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

因为HashMap保证key的唯一性,来实现HashSet而它的value值存的都是一个叫**PRESENT的new出来的Obejct对象!**
LinkedHashSet: 这个是一个 HashSet + LinkedList 的结构,特点就是既拥有了 O(1) 的时间复杂度,又能够保留插入的顺序。
TreeSet: 采用红黑树结构,特点是可以有序,可以用自然排序或者自定义比较器来排序;缺点就是查询速度没有 HashSet 快。
那每个 Set 的底层实现其实就是对应的 Map:
数值放在 map 中的 key 上,value 上放了个 PRESENT,是一个静态的 Object,相当于 place holder,每个 key 都指向这个 object。
那么具体的实现原理增删改查四种操作,以及哈希冲突hashCode()/equals() 等问题都在 HashMap 那篇文章里讲过了,这里就不赘述了。

3.10Comparable 和 Comparator 的区别

  • comparable 接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序
  • comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序

一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法,当我们需要对某一个集合实现两种排序方式,比如一个 song 对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo()方法和使用自制的Comparator方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort().

Comparator排序:

ArrayList<Integer> arrayList = new ArrayList<Integer>();
        arrayList.add(-1);
        arrayList.add(3);
        arrayList.add(3);
        arrayList.add(-5);
        arrayList.add(7);
        arrayList.add(4);
        arrayList.add(-9);
        arrayList.add(-7);
        System.out.println("原始数组:");
        System.out.println(arrayList);
        // void reverse(List list):反转
        Collections.reverse(arrayList);
        System.out.println("Collections.reverse(arrayList):");
        System.out.println(arrayList);

        // void sort(List list),按自然排序的升序排序
        Collections.sort(arrayList);
        System.out.println("Collections.sort(arrayList):");
        System.out.println(arrayList);
        // 定制排序的用法
        Collections.sort(arrayList, new Comparator<Integer>() {

            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        System.out.println("定制排序后:");
        System.out.println(arrayList);

Output:

原始数组:
[-1, 3, 3, -5, 7, 4, -9, -7]
Collections.reverse(arrayList):
[-7, -9, 4, 7, -5, 3, 3, -1]
Collections.sort(arrayList):
[-9, -7, -5, -1, 3, 3, 4, 7]
定制排序后:
[7, 4, 3, 3, -1, -5, -7, -9]

Comparable

// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他
// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了
public  class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        super();
        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;
    }

    /**
     * T重写compareTo方法实现按年龄来排序
     */
    @Override
    public int compareTo(Person o) {
        if (this.age > o.getAge()) {
            return 1;
        }
        if (this.age < o.getAge()) {
            return -1;
        }
        return 0;
    }
}


public static void main(String[] args) {
        TreeMap<Person, String> pdata = new TreeMap<Person, String>();
        pdata.put(new Person("张三", 30), "zhangsan");
        pdata.put(new Person("李四", 20), "lisi");
        pdata.put(new Person("王五", 10), "wangwu");
        pdata.put(new Person("小红", 5), "xiaohong");
        // 得到key的值的同时得到key所对应的值
        Set<Person> keys = pdata.keySet();
        for (Person key : keys) {
            System.out.println(key.getAge() + "-" + key.getName());

        }
    }

Output:

5-小红
10-王五
20-李四
30-张三

3.11 无序性和不可重复性的含义是什么

1、什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
2、什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法。

3.12 HashMap 和 Hashtable 的区别

  1. 线程是否安全:

HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);

  1. 效率:

因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;

  1. 对 Null key 和 Null value 的支持:

HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException

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

① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。
② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。

  1. 底层数据结构:

JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

**HashMap**** 中带有初始容量的构造函数:**

   public HashMap(int initialCapacity, float loadFactor) {
        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;
        this.threshold = tableSizeFor(initialCapacity);
    }
     public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

下面这个方法保证了 HashMap 总是使用 2 的幂作为哈希表的大小。

 /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

3.13. HashMap 和 TreeMap 区别

TreeMap 和HashMap 都继承自AbstractMap ,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口。
image.png
实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。
实现SortMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。示例代码如下:

/**
 * @author shuang.kou
 * @createTime 2020年06月15日 17:02:00
 */
public class Person {
    private Integer age;

    public Person(Integer age) {
        this.age = age;
    }

    public Integer getAge() {
        return age;
    }


    public static void main(String[] args) {
        TreeMap<Person, String> treeMap = new TreeMap<>(new Comparator<Person>() {
            @Override
            public int compare(Person person1, Person person2) {
                int num = person1.getAge() - person2.getAge();
                return Integer.compare(num, 0);
            }
        });
        treeMap.put(new Person(3), "person1");
        treeMap.put(new Person(18), "person2");
        treeMap.put(new Person(35), "person3");
        treeMap.put(new Person(16), "person4");
        treeMap.entrySet().stream().forEach(personStringEntry -> {
            System.out.println(personStringEntry.getValue());
        });
    }
}

输出:

person1
person4
person2
person3

可以看出,TreeMap 中的元素已经是按照 Person 的 age 字段的升序来排列了。
上面,我们是通过传入匿名内部类的方式实现的,你可以将代码替换成 Lambda 表达式实现的方式:

TreeMap<Person, String> treeMap = new TreeMap<>((person1, person2) -> {
  int num = person1.getAge() - person2.getAge();
  return Integer.compare(num, 0);
});

综上,相比于HashMap来说 TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。

3.14HashMap 的底层实现

JDK1.8 之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

JDK 1.8 HashMap 的 hash 方法源码:

JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。

  static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

对比一下 JDK1.7 的 HashMap 的 hash 方法源码.

static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
image.png

JDK1.8 之后

相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡 。同时数组长度小于64时,搜索时间相对要快些。所以综上所述为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树。
当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于8并且数组长度大于64时,链表转换为红黑树时,效率也变的更高效。
image.png
TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

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

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。

这个算法应该如何设计呢?

我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。

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

主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。
详情请查看:https://coolshell.cn/articles/9606.html

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

HashMap 的 7 种遍历方式与性能分析!

3.18 ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMapHashtable 的区别主要体现在实现线程安全的方式上不同。

  • 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
  • 实现线程安全的方式(重要):在 JDK1.7 的时候,**ConcurrentHashMap**(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了 **Segment** 的概念,而是直接用 **Node** 数组+链表+红黑树的数据结构来实现,并发控制使用 **synchronized** 和 CAS 来操作。(JDK1.6 以后 对 **synchronized** 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable**(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

两者的对比图:

HashTable:
image.png
JDK1.7 的 ConcurrentHashMap:
image.png

JDK1.8 的 ConcurrentHashMap:
image.png
JDK1.8 的 ConcurrentHashMap 不在是 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。当冲突链表达到一定长度时,链表会转换成红黑树。

3.19 ConcurrentHashMap 线程安全的具体实现方式/底层具体实现

JDK1.7(上面有示意图)

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成
Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。

JDK1.8 (上面有示意图)

ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。

3.20Collections 集合工具类

Collections 工具类常用方法:

  1. 排序
  2. 查找,替换操作
  3. 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)

1.5.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个元素整体移到后面

1.5.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), 用新元素替换旧元素

1.5.3. 同步控制

Collections 提供了多个synchronizedXxx()方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
我们知道 HashSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。Collections 提供了多个静态方法可以把他们包装成线程同步的集合。
最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。
方法如下:

synchronizedCollection(Collection<T>  c) //返回指定 collection 支持的同步(线程安全的)collection。
synchronizedList(List<T> list)//返回指定列表支持的同步(线程安全的)List。
synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的)Map。
synchronizedSet(Set<T> s) //返回指定 set 支持的同步(线程安全的)set。

3.21集合安全类

1.线程安全的List

  1. Vector类(不推荐)
  2. Collections.synchronizedList(List list);
  3. CopyOnWriteArrayList(推荐)

看看CopyOnWriteArrayList(写时复制)是怎么保证线程安全的!
先看怎么添加元素

    /** The array, accessed only via getArray/setArray. */
	//数组,只能通过 getArray/setArray 访问。
    private transient volatile Object[] array;

/**
     * Appends the specified element to the end of this list.
     *	将指定的元素附加到此列表的末尾。
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }


	  /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }


  1. 这里在添加元素的时候先加锁,复制原本的底层数组
  2. 复制好的新数组后,再将新添加的元素,添加到新数组尾部
  3. 将底层的array指向新的数组集合完成替换!

怎么获取元素

    
	 /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

	/**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }

   /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }

   private E get(Object[] a, int index) {
        return (E) a[index];
    }

因为写操作是加锁的,并不断在复制出新的数组,所以使用此类可以保证线程安全且不影响读操作,可以理解为缓存!

2.线程安全的Set

  1. CopyOnWriteArraySet
  2. Collections.synchronizedSet(Set s)

3.线程安全的Map

  1. HashTable(不推荐)
  2. Collections.synchronizedMap()
  3. ConcurrentHashMap(推荐)

3.22队列基础介绍!

image.png
1.什么是队列?
队列是数据结构中比较重要的一种类型(是一种数据结构),它支持 FIFO(first input first out),尾部添加、头部删除(先进队列的元素先出队列),跟我们生活中的排队类似。

2.什么情况下使用队列?
一般情况下,如果是对一些及时消息的处理,并且处理时间很短的情况下是不需要队列的,直接阻塞式的方法调用就可以了。但是如果在消息处理的时候特别费时间,这个时候如果有新消息来了,就只能处于阻塞状态,造成用户等待。这个时候便需要引入队列了。当接收到消息后,先把消息放到队列中,然后再用行的线程进行处理,这个时候就不会有消息阻塞了。

3.队列常用API

方式 (队列满了,或者空了)抛出异常** 有返回值,不抛出异常**阻塞 等待** 超时等待 **
添加addoffer()put()offer(,)
移除removepoll()take()poll(,)
检测队首元素elementpeek

3.23双端队列ArrayDeque和LinkedList

ArrayDeque:
ArrayDeque 是基于数组实现的可动态扩容的双端队列,也就是说你可以在队列的头和尾同时插入和弹出元素。当元素数量超过数组初始化长度时,则需要扩容和迁移数据。

Deque接口的可调整大小的数组实现。 数组双端队列没有容量限制; 它们根据需要增长以支持使用。 它们不是线程安全的; 在没有外部同步的情况下,它们不支持多线程并发访问。 禁止空元素。 此类用作Stack时可能比Stack快,用作队列时比LinkedList快

 //创建数组双端队列
        ArrayDeque arrayQueue = new ArrayDeque(12);
        //向队列中添加10个元素
        for (int i=1;i<=10;i++){
            System.out.println(arrayQueue.offer(i));
        }
        //添加元素到队列首
        arrayQueue.offerFirst("first");
        //添加元素到队列尾
        arrayQueue.offerLast("last");

        //如果队列不为空 就取出数据
        while (!arrayQueue.isEmpty()){
            System.out.println(arrayQueue.poll());
        }

//结果
true
true
true
true
true
true
true
true
true
true
    
first
1
2
3
4
5
6
7
8
9
10
last

3.24延迟队列 DelayQueue

你是否有时候需要把一些数据存起来,倒计时到某个时刻在使用?
在Java的队列数据结构中,还有一种队列是延时队列,可以通过设定存放时间,依次轮训获取。
不按照存储的顺序,按照时间来!!!

public class TestDelayed implements Delayed {

    private String str;
    private long time;

    public TestDelayed(String str, long time, TimeUnit unit) {
        this.str = str;
        this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return time - System.currentTimeMillis();
    }

    @Override
    public int compareTo(Delayed o) {
        TestDelayed work = (TestDelayed) o;
        long diff = this.time - work.time;
        if (diff <= 0) {
            return -1;
        } else {
            return 1;
        }
    }

    public String getStr() {
        return str;
    }


}


 public static void main(String[] args) throws InterruptedException {
        DelayQueue<TestDelayed> delayQueue = new DelayQueue<TestDelayed>();
        delayQueue.offer(new TestDelayed("aaa", 5, TimeUnit.SECONDS));
        delayQueue.offer(new TestDelayed("ccc", 1, TimeUnit.SECONDS));
        delayQueue.offer(new TestDelayed("bbb", 3, TimeUnit.SECONDS));

        System.out.println(((TestDelayed) delayQueue.take()).getStr());
        System.out.println(((TestDelayed) delayQueue.take()).getStr());
        System.out.println(((TestDelayed) delayQueue.take()).getStr());

		/*
ccc
bbb
aaa
		*/
    }

3.25.阻塞队列和非阻塞队列???

什么是阻塞???
阻塞和非阻塞指的是调用者在等待返回结果时的状态。阻塞时,在调用结果返回前,当前线程会被挂起,并在得到结果之后返回。非阻塞时,如果不能立刻得到结果,则该调用者不会阻塞当前线程。

//        Queue queue = new ArrayDeque(1);
//        while (true){
//
//           new Thread(()->{
//               System.out.println(Thread.currentThread().getName()+"普通队列取值--->"+queue.poll());
//           }).start();
//        }
//        /*
//        测试结果 会不断创建生成 获取队列中的值不会暂停
//         */

        BlockingQueue blockingQueue =new ArrayBlockingQueue(10);
        for (int i = 1; i <= 5; i++) {
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+"阻塞队列取值--->");
                    System.out.println(blockingQueue.take());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        /*
        如果取不到值 会一直等待有值为止
         */

3.26优先级的阻塞队列 PriorityBlockingQueue

PriorityBlockingQueue 类实现了 BlockingQueue 接口。
PriorityBlockingQueue 是一个无界的并发队列。
它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
注意 PriorityBlockingQueue 对于具有相等优先级(compare() == 0)的元素并不强制任何特定行为。同时注意,如果你从一个 PriorityBlockingQueue 获得一个 Iterator 的话,该 Iterator 并不能保证它对元素的遍历是以优先级为序的。以下是使用 PriorityBlockingQueue 的示例:

 BlockingQueue queue = new PriorityBlockingQueue();
        //String implements java.lang.Comparable
        queue.put("E");
        queue.put("A");
        queue.put("B");
        queue.put("C");
        queue.put("D");
        for (int i = 0; i < 5; i++) {
            System.out.println(queue.take());
        }
/*
A
B
C
D
E
*/

3.27 同步队列SynchronousQueue

怎么理解这个“特殊”的队列

一般印象中,阻塞队列都有缓冲,但是这个队列是没有缓冲的,就是说这里边不能存东西。数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲数据到队列中。可以这样来理解:生产者和消费者互相等待对方,握手(传数据,一个put,一个take),然后一起离开。

什么地方需要这个队列

  • 非常适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步。
  • 在线程池里的一个典型应用是Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
  • 全宇宙的JAVA IT人士应该都知道ThreadPoolExecutor的执行流程:

core线程还能应付的,则不断的创建新的线程;
core线程无法应付,则将任务扔到队列里面;
队列满了(意味着插入任务失败),则开始创建MAX线程,线程数达到MAX后,队列还一直是满的,则抛出RejectedExecutionException.
这个执行流程有个小问题,就是当core线程无法应付请求的时候,会立刻将任务添加到队列中,如果队列非常长,而任务又非常多,那么将会有频繁的任务入队列和任务出队列的操作。

根据实际的压测发现,这种操作也是有一定消耗的。其实JAVA提供的SynchronousQueue队列是一个零长度的队列,任务都是直接由生产者递交给消费者,中间没有入队列的过程,可见JAVA API的设计者也是有考虑过入队列这种操作的开销。

BlockingQueue<String> blockingQueue = new SynchronousQueue<>(); // 同步队列

        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+" put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName()+" put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName()+" put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T1").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+"=>"+blockingQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+"=>"+blockingQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+"=>"+blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T2").start();

3.28 阻塞队列中ArrayBlockingQueue和LinkedBlockingQueue的区别与联系

优缺点:
LinkedBlockingQueue的优点是锁分离,那就很适合生产和消费频率差不多的场景,这样生产和消费互不干涉的执行,能达到不错的效率

相同点
1、LinkedBlockingQueue和ArrayBlockingQueue都实现了BlockingQueue接口;
2、LinkedBlockingQueue和ArrayBlockingQueue都是可阻塞的队列
内部都是使用ReentrantLock和Condition来保证生产和消费的同步;
当队列为空,消费者线程被阻塞;当队列装满,生产者线程被阻塞;
使用Condition的方法来同步和通信:await()和signal()
不同点
1、由上图可以看出,他们的锁机制不同
LinkedBlockingQueue中的锁是分离的,生产者的锁putLock,消费者的锁takeLock
而ArrayBlockingQueue生产者和消费者使用的是同一把锁
2、他们的底层实现机制也不同
LinkedBlockingQueue内部维护的是一个链表结构:
image.png
在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大。
而ArrayBlockingQueue内部维护了一个数组:
image.png
在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例。
3、构造时的区别
LinkedBlockingQueue有默认的容量大小为:Integer.MAX_VALUE,当然也可以传入指定的容量大小。
image.png
ArrayBlockingQueue在初始化的时候,必须传入一个容量大小的值
观察其提供的构造方法就能知道:
image.png
4、统计元素的个数
LinkedBlockingQueue中使用了一个具有原子性的AtomicInteger对象来统计元素的个数。
image.png
ArrayBlockingQueue则使用int类型来统计元素
image.png
5 、LinkedBlockingQueue双锁带来的好处?
image.pngLinkedBlockingQueue底层的put()方法实现

3.29、为何ArrayBlockingQueue无法实现锁分离机制?

我们先来看看ArrayBlockingQueue中的enqueue入队操作:
image.png
这也解释了为何ArrayBlockingQueue无法实现锁分离机制,ArrayBlockingQueue底层的数组是一个循环数组,循环数组的存放地址会改变,每次操作后数组元素的下标都不确定,这样的操作无法进行原子化操作,简而言之就是CAS操作时内存位置的值和预期原值不相匹配,无法通过CAS机制的校验。

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值