Java面试题(一)----Java基础

基础 

Java中==和equals有什么区别?

 一个是运算符,一个是方法。

 ==

  • 如果比较的对象是基本数据类型,则比较数值是否相等;
  • 如果比较的是引用数据类型,则比较的是对象的内存地址是否相等。

因为Java只有值传递,对于==来说,不管是比较基本数据类型,还是引用数据类型的变量,其比较的都是值,只是引用类型变量存的值是对象的地址。引用类型对象变量其实是一个引用,它们的值是指向对象所在的内存地址。

 equals方法

比较对象的内容是否相同。

特例:

 8种基本数据类型的大小,以及他们的封装类

 

  1. int是基本数据类型,Integer是int的封装类,是引用类型。int默认值是0,而Integer默认值是null,所以Integer能区分出0和null的情况。
  2. 基本数据类型在声明时,系统会自动给它分配空间,而引用类型声明也只是分配了引用空间,必须通过实例化开辟数据空间后才可以赋值。
  3. 数组对象也是一个引用对象,将一个数组赋值给另一个数组时,只是复制了一个引用,所以通过某一个数组所做的修改,在另一个数组中也看得见
  4. 虽然Java语言中定义了boolean类型,但是在Java虚拟机中,没有专门的字节码指令用于处理boolean类型的值。相反,编译器将boolean类型的值编译成Java虚拟机中的int类型,其中0表示false,非0表示true。同样,boolean类型的数组在Java虚拟机中被编码为byte类型的数组。这是因为Java虚拟机的设计者们认为,使用int类型来代替boolean类型,不会对性能造成太大的影响,而且可以简化虚拟机的实现。

 重载和重写的区别

  • 重载(Overload):是指在一个类中定义多个方法,它们具有相同的名称,但具有不同的参数列表(个数、类型、顺序),一边在不同的情况下可以调用不同的方法,重载方法可以在一个类中定义,也可以在不同类种定义,只要它们的方法签名不同即可
    public class MathUtils {
        public static int sum(int a, int b) {
            return a + b;
        }
        
        public static double sum(double a, double b) {
            return a + b;
        }
        
        public static int sum(int a, int b, int c) {
            return a + b + c;
        }
    }

 

  • 重写(Override):是指在子类中重新定义(覆盖)父类中已有的方法,以便实现不同的功能或适应不同的需求。重写方法必须和父类中的方法具有相同的方法名称、参数列表和返回值类型,并且访问权限不能比父类中的方法更严格
    public class Animal {
        public void eat() {
            System.out.println("Animal is eating");
        }
    }
    
    public class Dog extends Animal {
        @Override
        public void eat() {
            System.out.println("Dog is eating");
        }
    }

 深拷贝和浅拷贝的区别是什么

 

  • 深拷贝:是指将一个对象复制到另一个对象,新对象与原对象不共享引用类型属性(如数组、集合、对象等),也就是说,新对象和原对象的引用类型属性指向的是不同的地址,修改其中一个对象中的引用类型属性,不会影响另一个对象中的属性值。
  • 浅拷贝:是指将一个对象复制到另一个对象,新对象与原对象共享引用类型属性,也就是说,新对象与原对象中的引用类型属性指向的是同一个地址,修改器中一个对象的引用类型属性,会影响到另一个对象的属性值,Java中的Object类提供了clone方法来实现浅拷贝

 Java创建对象有几种方式

 

  1. 使用new关键字
    public class MyClass {
       public MyClass() {
          System.out.println("对象已创建");
       }
    
       public static void main(String[] args) {
          MyClass obj = new MyClass();
       }
    }
  2. 使用Class类的newInstance方法
    public class MyClass {
       public MyClass() {
          System.out.println("对象已创建");
       }
    
       public static void main(String[] args) throws Exception {
          Class cls = Class.forName("MyClass");
          MyClass obj = (MyClass) cls.newInstance();
       }
    }
  3. 使用Constructor类的newInstance方法
    public class MyClass {
       public MyClass() {
          System.out.println("对象已创建");
       }
    
       public static void main(String[] args) throws Exception {
          Constructor<MyClass> constructor = MyClass.class.getConstructor();
          MyClass obj = constructor.newInstance();
       }
    }
  4. 使用clone方法
    public class MyClass implements Cloneable {
       public MyClass() {
          System.out.println("对象已创建");
       }
    
       public static void main(String[] args) throws Exception {
          MyClass obj1 = new MyClass();
          MyClass obj2 = (MyClass) obj1.clone();
       }
    }
  5. 使用反序列化
    import java.io.*;
    
    public class MyClass implements Serializable {
       public MyClass() {
          System.out.println("对象已创建");
       }
    
       public static void main(String[] args) throws Exception {
          MyClass obj1 = new MyClass();
          ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("myFile.txt"));
          out.writeObject(obj1);
          out.close();
    
          ObjectInputStream in = new ObjectInputStream(new FileInputStream("myFile.txt"));
          MyClass obj2 = (MyClass) in.readObject();
          in.close();
       }
    }

 获取一个类Class对象的方式有哪些

 

  1. 通过对象的getClass()方法获取
  2. 通过类名.class获取
  3. 通过Class.forName()方法获取
  4. 通过ClassLoader.loadClass()方法获取

 

 

 


 a=a+b和a+=b有什么区别

  • +=操作会进行隐式自动类型转换,例如这里的 a += b会隐式的将加操作的结果类型强制转换为持有结果的类型,而a = a + b则不会自动进行类型转换
    // 两个byte类型的变量相加时,结果会被自动提升为int类型。这种类型提升被称为"拓宽原始转换",它适用于所有原始类型,包括byte、short、char和int。
    byte a = 127;
    byte b = 127;
    a = a + b;      // 编译报错:不兼容的类型。实际为 int',需要 'byte'
    a += b;         // a = (byte)(a + b)

 final有哪些用法

final是Java中的关键字,可以用来修饰类、方法、变量等,它的主要作用是用于定义常量、防止继承、防止重写方法等

  1. 定义常量:使用final关键字定义的变量称为常量,它的值在定以后就不能被修改。常量命名规范一般是大写字母加下划线
  2. 用于防止继承:使用final关键字修饰的类不能被继承
  3. 防止重写方法:使用final关键字修饰的方法不能被子类重写
  4. 优化性能:使用final关键字可以优化代码性能。被final修饰的方法和变量在编译时就已经确定了值,因此在运行时不需要进行计算,可以减少运行时的开销,提高程序的执行效率。同时,被final修饰的方法,JVM会尝试将其内联,以提高运行效率
  5. 优化代码可读性:在代码中使用final关键字可以使代码更易读。通过将变量声明为final,可以明确其含义,使代码更易于理解和维护

static有哪些用法

static是Java中的关键字,可以用来修饰类、方法、变量等,它的主要作用是创建静态成员,可以通过类名直接访问,而不需要实例化对象

  1. 用于创建静态变量:使用static关键字定义的变量称为静态变量,它的值与所有该类的对象共享,并且可以直接通过类名访问
    public class Tmp {
        static String str = "Hello";
    }
    
    public class Main {
        public static void main(String[] args) {
            System.out.println(Tmp.str);
        }
    }

  2. 用于创建静态方法:使用static关键字定义的方法称为静态方法,同样可以直接通过类名调用
    public class Tmp {
        static void myMethod() {
            System.out.println("Hello");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Tmp.myMethod();
        }
    }

  3. 用于创建静态代码块:使用static关键字定义的代码块称为静态代码块,它在类加载时执行,且只执行一次,一般用于初始化静态变量
    public class MyClass {
        static List<String> myStaticList;
        
        static {
            // 从文件中加载数据并进行解析
            try {
                File file = new File("mydata.txt");
                BufferedReader reader = new BufferedReader(new FileReader(file));
                String line;
                myStaticList = new ArrayList<>();
                while ((line = reader.readLine()) != null) {
                    myStaticList.add(line);
                }
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        public static void main(String[] args) {
            System.out.println("My static list contains: " + myStaticList);
        }
    }

  4. 创建静态内部类:使用static关键字定义的内部类被称为静态内部类,它与外部类的对象无关,可以直接访问外部类的静态成员
    public class OuterClass {
        private static int staticVar = 1;
        private int instanceVar = 2;
    
        public static class StaticInnerClass {
            public void print() {
                // 静态内部类可以直接访问外部类的静态变量
                System.out.println("StaticVar from inner class: " + staticVar);
            }
        }
    
        public void createInnerClass() {
            // 不需要创建OuterClass实例,但是可以直接创建StaticInnerClass实例,并且使用它访问外部类的静态成员
            StaticInnerClass staticInnerClass = new StaticInnerClass();
            staticInnerClass.print();
        }
    }

Java自动装箱与拆箱

  • 装箱就是自动将基本数据类型转换为包装类型(int -> Integer);底层调用的是Integer的valueOf(int)方法
    int i = 10;
    Integer i = Integer.valueOf(10);

  • 拆箱就是自动将包装类型转换为基本数据类型(Integer -> int);底层调用的是intValue()方法
    Integer i = Integer.valueOf(10); 
    int j = i.valueOf(i);

    下面的代码会输出什么?

    public class Tmp {
        public static void main(String[] args) {
            Integer a = 100;
            Integer b = 100;
            Integer c = 200;
            Integer d = 200;
    
            System.out.println(a == b);
            System.out.println(c == d);
        }
    }
    true
    false

  • 为什么会出现这样的结果呢?输出表明a和b指向的是同一个对象,而c和d指向的不是同一个对象,我们来看一下Integer.valueOf()方法的底层源码
    /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

  • 从注释中我们可以看到,此方法将始终缓存-128到127之间的值。
  • 也就是如果数值在-128和127之间,就会返回IntegerCache.cache中已经存在的对象的引用,否则创建一个新的Integer对象。所以上面的代码中,a和b的数值为100,就是从缓存中取的已存在的对象,指向的是同一个对象,所以返回true;而c和d的值为200,并不在缓存中,所以是新建的Integer对象,所以返回false

接口和抽象类的区别


String

String, StringBuffer, StringBuilder区别

异常

  • Java中的异常分为两类:ErrorException,二者都是Throwale类的子类。
    • Error表示虚拟机本身的错误或资源耗尽等严重情况,应用程序不应该视图去捕获这些异常,例如OOM(OutOfMemoryError)SOF(StackOverFlowError)
    • Exception表示程序运行中的异常情况,应该对其进行捕获和处理,Exception又分为可检查异常(Checked Exception)不可检查异常(Unchecked Exception)
      • 可检查异常需要程序显式地捕获并处理,例如IOExceptionSQLException
      • 而不可检查异常一般是程序运行时遇到的无法处理的错误,如NullPointerExceptionArrayIndexOutOfBoundsException等,这些异常都继承自RuntimeException类,也被称为运行时异常,程序不需要显式地去捕获这类异常

OOM和SOF

  • OOM(OutOfMemory)即内存溢出,一般是指JVM内存不足以分配新对象,导致无法继续运行程序。出现OOM的情况很多,例如
    1. 程序中创建了太多的对象,占用了过多的内存空间
    2. 代码中存在内存泄漏,导致不再使用的对象没有被及时释放,导致内存空间被占用
    3. 虚拟机参数设置不合理,导致JVM无法分配足够的内存等
  • SOF(StackOverFlow)即栈溢出,一般是指线程请求的栈深度大于JVM所允许的深度,导致StackOverFlowError异常。出现SOF的情况也有很多,例如
    1. 递归调用层数过多,导致栈空间被耗尽
    2. 代码中存在死循环或循环调用,导致栈空间被耗尽
    3. 虚拟机参数设置不合理,导致栈空间太小等

 平时都是怎样处理异常的?

  1. 按照异常类型分类处理:对于不同的异常类型,我会根据实际情况进行不同待处理。例如对于业务异常,我通常会将异常信息记录到日志中,并给出友好提示;对于系统异常,我会打印异常的堆栈信息,将异常信息记录到日志中以便排查问题
  2. 异常不要吞掉:在处理异常时,我不会简单的将异常捕获并吞掉,而是尽可能的将异常处理完毕,避免出现未处理的异常导致系统不稳定或者出现非预期的问题
  3. 日志记录:在处理异常时,我通常会将异常信息记录到日志中,以便后续的问题排查与分析
  4. 异常处理要及时:及时处理异常可以避免问题的扩大和影响范围的扩大,同时也可以减轻排查问题的难度
  5. 代码的健壮性:尽可能的在代码的设计和编写阶段考虑各种异常情况,图稿代码的健壮性,减少出现异常的可能性w

IO

字节流和字符流的区别?

都有哪些流

  • Java中的IO流是Java提供的一种用于输入和输出数据的机制,主要分为字节流和字符流两种类型,它们可以用于读取和写入不同种类的数据源,例如文件、网络连接、内存缓冲区等。具体来说,Java中的IO流可以分为以下几种类型
    1. 字节流(InputStream和OutStream):以字节为单位读写数据,适用于读写二进制文件和图片等数据
    2. 字符流(Reader和Writer):以字符为单位读写数据,适用于读写文本文件
    3. 缓冲流(BufferedInputSteam、BufferedOutputSteam、BufferedReader和BufferedWriter):在字节流和字符流的基础上增加了缓冲功能,提高读写数据的效率
    4. 对象流(ObjectInputSteam和ObjectOutputStream):用于序列化和反序列化Java对象,将Java对象转换为字节流进行存储和传输
    5. 转换流(InputStreamReader和OutputStreamWriter):将字节流转换为字符流或将字符流转换为字节流,提供了从字节流读取Unicode字符的方法
    6. 文件流(FileInputStream和FileOutputStream):用于读写文件,支持读写字节和字节数组
    7. 管道流(PipedInputStream和PipedOutputStream):用于线程之间的数据传输
  • 通过使用不同类型的IO流,可以很方便地完成文件的读写、网络数据的传输、对象的序列化等操作

JavaIO和NIO的区别

  • Java中的IO(Input/Output)是指对数据的输入和输出操作,其中包含了许多输入输出流。Java的IO主要基于阻塞式IO模型实现的,即在读写数据时会一直阻塞,直到数据读写完成,而NIO(NEW IO)是Java1.4引入的一组新IO API,也成为non-nlocking IO。NIO主要是基于非阻塞式IO模型实现,可以在单个线程上进行多个IO操作,提高了IO效率
  • 一下是Java IO和NIO的主要区别
    1. IO是面向流的,而NIO是面向缓冲区的。Java的IO中,数据总是通过InputStream或OutputStream等流的形式传输,而在NIO中,数据是从通道读入缓冲区,从缓冲区写入通道
    2. IO是阻塞的,而NIO是非阻塞的。Java的IO读取或写入数据时,会一直阻塞当前线程,直到操作完成或发生异常,而在NIO中,可以进行异步读写操作,即一个线程可以处理多个连接
    3. IO是单向的,而NIO是双向的。Java中的IO是单向的,即一个输入流只能读取数据,一个输出流只能写入数据,而在NIO中,缓冲区既可以读,也可以写
    4. IO使用字节流和字符流进行操作,而NIO使用Channel和Buffer进行操作。在Java的IO中,数据总是通过InputStream和OutputStream等流的形式传输,可以进行字节流和字符流的操作。而在NIO中,数据是从通道读入缓冲区,可以使用ByteBuffer、CharBuffer等缓冲区进行读写操作

反射

Java反射的作用与原理

  • Java反射是指在程序运行时动态地获取类的信息并操作类的属性方法构造器等,它允许程序在运行时动态地创建对象调用方法获取字段值等。Java反射的作用非常广泛,例如在框架ORM映射RPC调用等领域都有应用
  • Java反射的原理是通过Java的类加载机制,在运行时获取类的信息,包括类名、方法名、字段名、注解等,并生成类的Class对象,这个Class对象提供了操作类的各种方法和属性的API。反射可以通过Class类的一些方法来获取ConstructorMethodFiled等类的信息,通过这些信息可以实现对类的实例化调用方法获取字段值等操作
  • Java反射的主要优点是可以动态地加载类和调用类的方法、字段等,使得程序具有更高的灵活性和扩展性。不过由于反射是一种非常底层的操作,使用不当也容易导致性能问题,同时反射也存在安全隐患,因此在使用反射时需要谨慎处理


**集合**

Java集合,也叫作容器,主要是由两大接口派生而来:一个是Collection接口,主要用于存放单一元素;另一个是Map接口,主要用于存放键值对。对于Collection接口,下面又有三个主要的子接口:List、Set、Queue

注:图中只列举了主要的继承派生关系,并没有列举所有关系。比方省略了AbstractListNavigableSet等抽象类以及其他的一些辅助类,如想深入了解,可自行查看源码

List、Set、Queue、Map四者的区别?

  • List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
  • Set(注重独一无二的性质): 存储的元素不可重复的。
  • Queue(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
  • Map(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值

ArrayList和LinkedList区别

  • 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全;
  • 底层数据结构: ArrayList 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)

ArrayList和Vector的区别?

  • ArrayList 是 List 的主要实现类,底层使用 Object[]存储,适用于频繁的查找工作,线程不安全 。
  • Vector 是 List 的古老实现类,底层使用Object[] 存储,线程安全

Vector和Stack的区别?

  • Vector 和 Stack 两者都是线程安全的,都是使用 synchronized 关键字进行同步处理。
  • Stack 继承自 Vector,是一个后进先出的栈,而 Vector 是一个列表。

如何线程安全的操作ArrayList

ArrayList扩容的原理

ArrayList 是一个数组结构的存储容器,默认情况下,数组的长度是 10. 当然我们也可以在构建 ArrayList 对象的时候自己指定初始长度。 随着在程序里面不断地往 ArrayList 中添加数据,当添加的数据达到 10 个的时候, ArrayList 就没有多余容量可以存储后续的数据。 这个时候 ArrayList 会自动触发扩容。 扩容的具体流程很简单,

1. 首先,创建一个新的数组,这个新数组的长度是原来数组长度的 1.5 倍。

2. 然后使用 Arrays.copyOf 方法把老数组里面的数据拷贝到新的数组里面。 扩容完成后再把当前要添加的元素加入新的数组里面,从而完成动态扩容的过程。


HashSet、LinkedHashSet和TreeSet三者的区别

  • HashSetLinkedHashSetTreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。
  • HashSetLinkedHashSetTreeSet 的主要区别在于底层数据结构不同。HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO(先进先出)。TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。
  • 底层数据结构不同又导致这三者的应用场景不同。HashSet 用于不需要保证元素插入和取出顺序的场景,LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet 用于支持对元素自定义排序规则的场景

 


HashMap和Hashtable的区别

  • 线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!)
  • 效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它;
  • 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException

  • 初始容量大小和每次扩充容量大小的不同: ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable 没有这样的机制。
  • 哈希函数的实现HashMap 对哈希值进行了高位和低位的混合扰动处理以减少冲突,而 Hashtable 直接使用键的 hashCode() 值。

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;
    }

HashMap和HashSet区别

如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()writeObject()readObject()HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。

  • HashMap:将key.hashCode()作为hash值存放,将value直接作为value。
  • HashSet:调用HashMap的put方法;将key.hashCode()作为hash值存放,将HashSet类的final变量PRESENT作为value。

HashMap,TreeMap,LinkedHashMap的区别

  • 都属于Map;
    • Map 主要用于存储键(key)值(value)对,根据键得到值,因此键不允许键重复,但允许值重复。
  • 都是线程不安全的

JDK8 HashMap的改变

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

TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

HashMap的底层原理

HashMap其实就是数组,将key的hashCode对数组长度取余作为数组的下标,将value作为数组的值。如果key的hashCode重复(即:数组的下标重复),则将新的key和旧的key放到链表中。

若链表长度大于8 且容量小于64 会进行扩容;若链表长度大于8 且数组长度大于等于64,会转化为红黑树(提高定位元素的速度);若红黑树节点个数小于等于6,则将红黑树转为链表。

hash冲突的4种解决方案

再哈希法

提供多个哈希函数,如果第一个哈希函数计算出来的key的哈希值冲突了,则使用第二个哈希函数计算key的哈希值。

优点

  1. 不易产生聚集

缺点

  1. 增加了计算时间

建立公共溢出区

将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

链地址法

对于相同的哈希值,使用链表进行连接。(HashMap使用此法

优点

  1. 处理冲突简单,无堆积现象。即非同义词决不会发生冲突,因此平均查找长度较短;
  2. 适合总数经常变化的情况。(因为拉链法中各链表上的结点空间是动态申请的)
  3. 占空间小。装填因子可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计
  4. 删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

缺点

  1. 查询时效率较低。(存储是动态的,查询时跳转需要更多的时间)
  2. 在key-value可以预知,以及没有后续增改操作时候,开放定址法性能优于链地址法。
  3. 不容易序列化

HashMap扩容的原理

上表中的“容量”其实就是数组长度。

HashMap中,哈希桶数组table的长度length大小必须为2的n次方(非质数),这是一种非常规的设计,常规的设计是把桶的大小设计为质数。相对来说质数导致冲突的概率要小于非质数,Hashtable初始化桶大小为11,就是桶大小设计为质数的应用(Hashtable扩容后不能保证还是质数)。

何时扩容

HashMap是懒加载,构造完HashMap对象后,若没用 put 来插入元素,HashMap不会去初始化或者扩容table,此时table是空的。扩容有如下场景:

  1. 首次调用put方法时,HashMap会发现table为空然后调用resize方法进行初始化。
  2. 非首次调用put方法时,若HashMap发现size(元素个数)大于threshold(阈值)(数组长度乘以加载因子的值),则会调用resize方法进行扩容。
  3. 链表长度大于8 且数组长度小于64 会进行扩容。
    • 链表长度大于8 (且数组长度大于等于64),会转化为红黑树。

数组是无法自动扩容的,所以只能是换一个更大的数组去装填以前的元素和将要添加的新元素。

resize()概述

  1. 判断扩容前的旧数组容量是否已经达到最大(2^30)了
    1. 若达到则修改阈值为Integer的最大值(2^31 – 1),以后就不会扩容了。
    2. 若没达到,则修改数组大小为原来的2倍
  2. 以新数组大小创建新的数组(Node<K, V>[])
  3. 将数据转移到新的数组(Node[])里
    • 不一定所有的节点都要换位置。比如:原数组大小为16,扩容后为32。若原来有hash值为1和17两个数据,他们对16取余都是1,在同一个桶里;扩容后,1对32取余仍然是1,而17对32取余却成了17,需要换个位置。(对应的代码为:if ((e.hash & oldCap) == 0)  若为true,则不需要换位置。
  4. 返回新的Node<K, V>[] 数组

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

 JDK1.7 及之前版本的 HashMap 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。

为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用 ConcurrentHashMap 。 

 HashMap为什么线程不安全

 JDK1.7 及之前版本,在多线程环境下,HashMap 扩容时会造成死循环和数据丢失的问题。

数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK 1.8 为例进行介绍。

JDK 1.8 后,在 HashMap 中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 HashMapput 操作会导致线程不安全,具体来说会有数据覆盖的风险。

举个例子:

  • 两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)。
  • 不同的线程可能在不同的时间片获得 CPU 执行的机会,当前线程 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。
  • 随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所有此时会直接进行插入,这就导致线程 2 插入的数据被线程 1 覆盖了。

HashMap线程安全的操作方法

ConcurrentHashMap的原理?JDK8有什么改变?

JDK8中ConcurrentHashMap结构基本上和HashMap一样,采用了HashMap(数组 + 链表 + 红黑树) + synchronized + CAS 的实现方式来设计。读操作使用volatile,写操作使用synchronized 和CAS。

CAS:在判断数组中当前位置为null的时候,使用CAS把这个新的Node写入数组中对应的位置。

synchronized :当数组中的指定位置不为空时,通过加锁来添加这个节点(链表或者红黑树)。

JDK8中采用的是Node(放弃了Segment)。Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。


HashMap和ConcurrentHashMap的区别

  • 相似点:
    1. 都是Map接口的实现类,底层数据结构都是哈希表(数组+链表/红黑树)
    2. 都允许存储键值对,key和value都可以为null
    3. 都支持快速的插入、删除和查找操作
  • 不同点
    1. 线程安全型:HashMap是非线程安全的,而ConcurrentHashMap是线程安全的。在多线程环境下,ConcurrentHashMap的表现更优
    2. 性能:在并发场景下,ConcurrentHashMap要比HashMap表现更好,尤其是当写操作很多的情况下。因为ConcurrentHashMap使用了分段锁的机制,使得多线程能够同时操作不同的段,减少了线程的竞争,从而提高了并发的效率
    3. 扩容机制:HashMap扩容时会将原来的数组复制到新的更大的数组中,然后重新计算每个元素在新数组中的位置,这个过程比较耗时。而ConcurrentHashMap在扩容时,只需要复制里面的一部分段,不需要复制整个Map,因此速度相对更快
    4. null key和null value:HashMap允许key和value都是null,但是ConcurrentHashMap不允许key和value为null
  • 总体来说,如果在多线程环境下需要使用Map,建议使用ConcurrentHashMap,否则使用HashMap即可。

ConcurrentHashMap和Hashtable区别

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

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


 

红黑树有哪几个特征

  • 红黑树是一种自平衡的二叉搜索树,具有以下特征
    1. 每个节点要么是黑色,要么是红色
    2. 根节点是黑色的
    3. 所有叶子结点都是黑色的空节点(NIL节点)
    4. 如果一个节点是红色的,则它的那个子节点都是黑色
    5. 任意一个节点到其每个叶子结点的所有路径都包含相同数目的黑色节点
  • 这些特征保证了红黑树在插入和三处节点时能够保持平衡,从而保证了其查找、插入、删除操作的时间复杂度都是O(log n)级别的


JDK8新特性 

接口允许default和static;lambda;stream;时间新API(LocalDateTime等)

.................................未完待续.............................

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值