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

news2024/11/14 13:44:05

基础 

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等)

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2034918.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SQL Zoo 9-.Window functions

以下数据均来自SQL Zoo 1.Show the lastName, party and votes for the constituency S14000024 in 2017.&#xff08;显示2017年选区“S14000024”的姓氏、政党和选票&#xff09; SELECT lastName, party, votesFROM geWHERE constituency S14000024 AND yr 2017 ORDER BY…

开启IDEA打开新项目时窗口提示

1.背景 实际开发中很多时候,我们会同时打开多个项目,或者项目切换, 这时候有2中情况,打开新窗口或者在当前窗口打开(即:关闭当前窗口,打开新窗口) 具体是那种情况,要根据实际场景,因此我们希望可以弹框提示: 是打开新窗口,还是在当前窗口打开 具体设置如下: 2.步骤 3.测试…

中国科技统计年鉴,数据覆盖1991-2022年多年份

基本信息. 数据名称: 中国科技统计年鉴 数据格式: excel 数据时间: 1991-2022年 数据几何类型: xlsx 数据坐标系: WGS84 数据来源&#xff1a;国家统计局 数据预览&#xff1a; 数据可视化.

zabbix“专家坐诊”第251期问答

问题一 Q&#xff1a;zabbix模板自带的监控项很多都不需要&#xff0c;也不用删除&#xff0c;就是怎么让他们都不展示出来 A&#xff1a;禁用掉 Q&#xff1a;还是在的&#xff0c;我想要就看不见&#xff0c;不是不启用&#xff0c;想要效果跟删除一样&#xff0c;看不见&am…

我在高职教STM32——I2C通信之SHT20传感器(1)

大家好,我是老耿,高职青椒一枚,一直从事单片机、嵌入式、物联网等课程的教学。对于高职的学生层次,同行应该都懂的,老师在课堂上教学几乎是没什么成就感的。正是如此,才有了借助CSDN平台寻求认同感和成就感的想法。在这里,我准备陆续把自己花了很多心思设计的教学课件分…

go-zero中基本配置及获取参数

一、使用配置文件启动项目 1、在项目的etc文件夹下分别创建开发环境和测试环境的配置文件,这里简单点使用不同的端口 2、配置Makefile文件启动命令来启动不同配置文件 runDev:go run users.go -f etc/application-dev.yml runProd:go run users.go -f etc/application-prod.ym…

RC电路(三):零点和极点

一、零极点定义 零点和极点是在自动控制原理中用于描述系统特性的概念。‌ 零点&#xff08;Zero&#xff09;&#xff1a;‌在传递函数的分子多项式等于零的解。即当系统的输入信号等于零时&#xff0c;‌输出信号不为零的情况下&#xff0c;‌输入信号与输出信号相等的点。‌…

人工智能系统测试生命周期详解之测试数据准备

前面的文章里我们已经整体介绍过了人工智能测试的生命周期&#xff0c;它需要经历测试需求的分析、测试环境的准备、数据的准备与验证、测试的执行预分析以及上线后的监控这样一个过程。前面的文章已经为大家介绍了人工智能系统测试生命周期的“需求分析”环节和“测试环境准备…

SPSS-主成分分析实践

相信各位小伙伴都知道主成分分析的原理&#xff0c;我们今天用SPSS来实现一下主成分分析 主成分分析步骤 对原来的全部指标进行标准化&#xff0c;以消除变量在水平和量纲的影响根据标准化的数据矩阵求出相关系数矩阵求出协方差矩阵的特征根和特征向量确定主成分&#xff0c;…

DRM(Direct Rendering Manager)直接渲染管理

DRM是Linux 内核的一个子系统&#xff0c;负责与现代显卡的GPU进行交互。DRM 公开了一个API (libdrm)&#xff0c;用户空间程序可以使用该API 向 GPU 发送命令和数据并执行诸如配置显示器模式设置之类的操作。DRM 最初是作为X 服务器直接渲染基础架构的内核空间组件开发的&…

大规模复杂场景三维重建与理解——学习笔记

一、完整的大规模复杂场景三维重建与理解系统 一个完整的大规模复杂场景三维重建与理解系统包含“自主式场景数据获取->高精度联合位姿解算->完整化三维几何重建->细粒度三维语义分割->结构化三维矢量表达->全天候长时定位定姿->高时效地图增量更新”等模块。…

2022年第一至第四批专精特新“小巨人”企业数据,企业名称、经营范围、公示批次等字段可查询

基本信息. 数据名称: 第一至第四批专精特新“小巨人”企业数据 数据格式: Shpxlsx 数据时间: 2022年 数据几何类型: 点 数据坐标系: WGS84坐标系 数据来源&#xff1a;《中国城市统计年鉴》中统计的工业企业数相关数据&#xff0c;对象为地级及以上的城市&#xff0c;统计…

【书生大模型实战营第三期 | 进阶岛第2关-Lagent 自定义你的 Agent 智能体】

学习心得&#xff1a;Lagent 自定义你的 Agent 智能体 摘要 Lagent 是一个为大语言模型设计的轻量级开源智能体框架&#xff0c;它不仅支持多种智能体范式&#xff0c;如 AutoGPT、ReAct&#xff0c;还集成了多种工具&#xff0c;包括但不限于 Arxiv 搜索、Google 搜索等。通…

电子行业数字工厂管理系统解决方案

电子行业数字工厂管理系统解决方案是针对电子企业特定需求而设计的一套综合管理系统&#xff0c;旨在通过数字化手段提升生产效率、优化资源配置、降低运营成本&#xff0c;并确保高品质产品的输出。以下是一个详细的电子行业数字工厂管理系统解决方案的概述&#xff1a; 一、系…

C语言:字符函数,字符串函数

在编程的过程中&#xff0c;我们经常要处理字符和字符串&#xff0c;为了方便操作字符和字符串&#xff0c;C语言标准库中提供了一系列库函数。 一. 字符分类函数 C语言中有一系列的函数是专门做字符分类的&#xff0c;也就是一个字符是属于什么类型的字符的。 这些函数的使用…

你知道AI模型是如何学习的吗?

在人工智能的广阔天地中&#xff0c;AI模型的学习方式不仅决定了其智能行为的深度和广度&#xff0c;更是推动技术进步和应用创新的关键动力。随着AI技术的飞速发展&#xff0c;我们越来越意识到&#xff0c;深入了解AI的学习机制对于把握其潜能至关重要。 我们将从基础概念出…

hive之greatest和least函数

1、greatest函数&#xff1a; greatest(col_a, col_b, ..., col_n)比较n个column的大小&#xff0c;过滤掉null或对null值进行处理&#xff0c;当某个column中是string&#xff0c;而其他是int/double/float等时&#xff0c;返回null&#xff1b; 举例&#xff1a; select g…

Python 中的变量赋值、多重赋值

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 在编程中&#xff0c;变量赋值是最基础的操作之一。Python 作为一门动态类型语言&#xff0c;其变量赋值和多重赋值具有独特的灵活性和简洁性。本文将详细介绍 Python 中的变量赋值、多重赋值&#xff0c;并包含…

基于Ascend C的Matmul算子性能优化最佳实践

矩阵乘法是深度学习计算中的基础操作&#xff0c;对于提升模型训练和推理速度至关重要。昇腾AI处理器是一款专门面向AI领域的AI加速器&#xff0c;其AI Core采用达芬奇架构&#xff0c;以高性能Cube计算引擎为基础&#xff0c;针对矩阵运算进行加速&#xff0c;可大幅提高单位面…

JavaScript 逆向爬取实战

准备介绍&#xff1a; 当我们学习完整个 JS 逆向技巧后&#xff0c;这里是一次完整的分析爬取实战 案例介绍 本节案例网站不仅在 API 参数有加密&#xff0c; 而且前端 JS 也带有压缩混淆&#xff0c;其前端压缩打包工具使用 webpack , 混淆工具使用 javascript-obfuscator 。…