0%

Java基础

面试常问:

基础

  • 概念辨别:
    • JDK Java Development Kit 是Java的开发工具包,是Java的一个SDK
    • JRE Java Runtime Environment Java运行环境
    • SDK Software Development Kit 软件开发工具包
    • Java9 之后部分jdk和jre了
    • JIT 运行时编译,当JIT编译器第一次编译之后,会把字节码对应的机器码保存下来,下次可以直接使用,如果是热点代码就使用JIT进行编译启动,如果不是热点数据,就使用解释器来执行。
    • AOT:在程序执行前将其编译成机器码,属于静态编译。适合云原生场景
  • 基础数据类型:
    • == 和 equals : == 比较的是内存地址是否一致,equals比较的是对象的内容是否相等,Object类中没有区别,Striing,Integer等就有区别了
    • 基本数据类型:byte 8bit ,short 16 ,int 32 ,long 64,float 32 ,double 64,boolean 1,char , 注意char的默认值是 \u0000 也就是表示null的字符
    • StringBuffer可以看作是线程安全的StringBuilder
    • Map和Set:
      • 为什么HashMap的长度(桶的数量是2的幂次方),因为可以优化哈希值的分布,哈希值与长度-1进行位运算而不是取模,可以加快效率,也能分布更均匀,方便扩容
      • HashSet的底层是是用来一个HashMap,key为set的元素,value为一个固定的Object对象
      • HashMap的查询:底层实现数组+链表 1.8之后多了红黑树
        1. 没有哈希冲突,O(1)
        2. 有冲突,就会把冲突的键放在同一个桶的链表中,需要查询链表O(n);
        3. Java8之后,当桶中的数据达到一定规模就会转为红黑树,O(logn)
        • put方法:
          1. 判断key对数组table,是否为null,否则执行resize进行扩容(初始化)
          2. 根据key计算hash,得到数组索引
          3. table[i] == null 直接添加
          4. 不成立:
            • 判断table[i] 的首个元素是否和key一样,如果相同直接覆盖value
            • table[i] 是否为treeNode ,也就是是否是红黑树,如果是直接在树种插入键值对
            • 遍历table[i] 在尾部插入数据,如果长度大于8转为红黑树
          5. 判断实际数量是否超过了最大容量*0.75,如果超过进行扩容
        • 如何扩容:
          • 每次到达数组长度* 0.75时扩容,,每次扩容长度是原来最大容量的两倍
          • 扩容之后要把老数组移到新数组钟
        • 寻址算法:
          • 计算出key的hashCode,然后在这个值右移16位后的二进制及逆行按位异或运算,得到的hash
      • HashSet如何比较是否重复:根据hashcode来比较,如果相等,那么再调用equals方法来比较
    • 字节码:JVM可以理解的代码就是字节码,也就是.class文件
      • Java代码先经过编译生成字节码,之后由java解释器来解释执行
      • 面向对象三大特点:封装、继承、多态
      • 比较对象比较的是内存地址,而equals()没有重写时,也是比较的地址
      • 序列化:将数据结构或者对象转换成二进制字节流的过程
      • 包装类型的常量池:包装类型保存了一定范围内的所有的实例,可以减少内存的使用
    • 引用类型:
      1. 强引用:使用new
      2. 软引用:只有这个方式时,内存不足时会被回收
      3. 弱引用:只有这个方式是,内存重组也会被回收
      4. 虚引用:无法通过他获取对象
  • IO
    • BIO同步阻塞IO一直等待内核把数据拷贝到用户空间
    • NIO非阻塞IO,可以看作多路复用IO
    • AIO异步IO
    • NIO非阻塞IO Netty使用

多态的原理

动态绑定,运行时才将方法调用和方法实现关联起来。

静态方法为什么不能调用非静态成员

静态方法在类加载时就会分配内存,而非静态成员属于实例对象,所以调用不到,属于非法操作

字符串常量池

对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。

String为什么不可变

  1. String中的byte数组被private和final修饰,并且没有具体的方法暴露
  2. String对象使用final修饰,不可被继承,保证不会被子类破坏不可变性

== 和 equals区别

== :
基本数据类型是比较的值,对象比较的是地址
equals:
没重写的话比较的是地址,String是专门重写过的,比较的是具体的值是否相同

三种拷贝方式的区别

  1. 引用拷贝:不同的引用指向相同的对象
  2. 浅拷贝:外部对象是new 出来的新对象,内部对象仍然是指向一个
  3. 深拷贝:外部对象和内部对象都是新new 出来的

concurrenthashmap和hashmap和hashtable的区别

HashMap线程不安全
HashTable和concurrentHashMap是线程安全的HashMap,HashTable上锁时,会锁住整个表,而ConcurrentHashMap只会锁对应的段,使用的是分段锁。

序列化

SerialVersionUID

即使被static修饰,也会被序列化进二进制流中,用来判断序列化对象的版本一致性。

代理

代理模式是使用代理对象来替代真实对象,从而在不修改原目标的前提下提供额外的功能操作,拓展对象功能。

动态代理和静态代理的区别

  1. 静态代理在编译阶段就将接口、实现类、代理类都变成一个个实际的class文件。对于每一个被代理的对象都需要单独写一个代理类,非常不方便
  2. 动态代理:是在运行时动态生成字节码,并加载到JVM中。核心是 InvocationHandler 接口和 Proxy 类

JDK和CGLIB的区别

JDK是面向接口的,而CGLib是通过直接吗底层继承要代理的类来实现的,底层是asm

Spring AOP的底层实现主要基于动态代理模式。具体来说,有两种主要的实现方式:JDK 动态代理和 CGLIB 动态代理。Spring AOP 在运行时会根据目标对象的类型和配置来选择使用 JDK 动态代理还是 CGLIB 动态代理。如果目标对象实现了接口,并且没有强制要求使用 CGLIB 代理,Spring 会优先使用 JDK 动态代理。如果目标对象没有实现接口,或者通过配置强制使用 CGLIB 代理,那么 Spring 会使用 CGLIB 动态代理来实现 AOP。

BigDecimal工具类

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * 简化BigDecimal计算的小工具类
 */
public class BigDecimalUtil {

    /**
     * 默认除法运算精度
     */
    private static final int DEF_DIV_SCALE = 10;

    private BigDecimalUtil() {
    }

    /**
     * 提供精确的加法运算。
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和
     */
    public static double add(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.add(b2).doubleValue();
    }

    /**
     * 提供精确的减法运算。
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差
     */
    public static double subtract(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.subtract(b2).doubleValue();
    }

    /**
     * 提供精确的乘法运算。
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积
     */
    public static double multiply(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.multiply(b2).doubleValue();
    }

    /**
     * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
     * 小数点以后10位,以后的数字四舍五入。
     *
     * @param v1 被除数
     * @param v2 除数
     * @return 两个参数的商
     */
    public static double divide(double v1, double v2) {
        return divide(v1, v2, DEF_DIV_SCALE);
    }

    /**
     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
     * 定精度,以后的数字四舍五入。
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 表示表示需要精确到小数点以后几位。
     * @return 两个参数的商
     */
    public static double divide(double v1, double v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.divide(b2, scale, RoundingMode.HALF_EVEN).doubleValue();
    }

    /**
     * 提供精确的小数位四舍五入处理。
     *
     * @param v     需要四舍五入的数字
     * @param scale 小数点后保留几位
     * @return 四舍五入后的结果
     */
    public static double round(double v, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b = BigDecimal.valueOf(v);
        BigDecimal one = new BigDecimal("1");
        return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue();
    }

    /**
     * 提供精确的类型转换(Float)
     *
     * @param v 需要被转换的数字
     * @return 返回转换结果
     */
    public static float convertToFloat(double v) {
        BigDecimal b = new BigDecimal(v);
        return b.floatValue();
    }

    /**
     * 提供精确的类型转换(Int)不进行四舍五入
     *
     * @param v 需要被转换的数字
     * @return 返回转换结果
     */
    public static int convertsToInt(double v) {
        BigDecimal b = new BigDecimal(v);
        return b.intValue();
    }

    /**
     * 提供精确的类型转换(Long)
     *
     * @param v 需要被转换的数字
     * @return 返回转换结果
     */
    public static long convertsToLong(double v) {
        BigDecimal b = new BigDecimal(v);
        return b.longValue();
    }

    /**
     * 返回两个数中大的一个值
     *
     * @param v1 需要被对比的第一个数
     * @param v2 需要被对比的第二个数
     * @return 返回两个数中大的一个值
     */
    public static double returnMax(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.max(b2).doubleValue();
    }

    /**
     * 返回两个数中小的一个值
     *
     * @param v1 需要被对比的第一个数
     * @param v2 需要被对比的第二个数
     * @return 返回两个数中小的一个值
     */
    public static double returnMin(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.min(b2).doubleValue();
    }

    /**
     * 精确对比两个数字
     *
     * @param v1 需要被对比的第一个数
     * @param v2 需要被对比的第二个数
     * @return 如果两个数一样则返回0,如果第一个数比第二个数大则返回1,反之返回-1
     */
    public static int compareTo(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.compareTo(b2);
    }

}

UnSafe类

提供一些很底层的操作,JUC中经常使用。
核心功能:

  1. 内存操作
  2. [[多线程#内存屏障|内存屏障]]
  3. 对象操作
  4. 数据操作
  5. CAS操作
  6. 线程调度
  7. Class操作
  8. 系统信息

并发相关

线程池

线程池的原理

初始化一个线程池,指定线程池的大小,也就是线程池中的线程数量
然后每次向线程池中提交任务,线程池中的线程循环从任务队列中去除任务并且执行
如果一个线程执行完一个任务之后,就会回到线程池,而不是销毁

线程池通过循环利用线程,而不是销毁避免了频繁创建和销毁线程池的开销

线程池中关于时间的参数起什么作用

  1. 空闲线程存活时间:当线程中的线程数量超过核心线程数量时,这些额外的线程在超过空闲线程存活时间就会被中止
  2. 任务超时时间:执行任务执行的最大时间,如果
  • 多线程:
    • 线程不安全的集合:
      • ArrayList,HashMap,HashSet,LinkedList等
        • ArrayList的实现:
          • add方法:确保数组在已使用长度(size) + 1之后能够存下洗一个数据
          • 计算数组的容量,如果当前数组已使用的长度+1后的大于当前数组的长度,使用grow方法扩容,扩大约1.5倍
          • 确保新增的数据有地方存储之后奖新元素加到位于size的位置上
          • 返回bool值
        • ArrayList list=new ArrayList(10)中的list 不会扩容
        • 使用asList后,原数组修改会改变新生成的List,因为他们最终指向的都是一个内存地址
        • 如何处理linkedlist和arraylist的线程不安全:
          1. 优先在方法内使用,定义为局部变量
          2. 使用synchornizedList来替换ConcurrentLinkedQueue
      • 使用toArray后,修改List的内容,数组的内容不会改变
      • Vector和ArrayList的区别:Vector是线程安全的,大部分方法是同步到,性能上ArrayList好一点,以为不需要同步,扩容的时候,A增加50%,V怎加100%
      • ConcurrentHashMap如何保证线程安全:
        1. 使用CAS(Compare and Swap),每次更新时,对比内存位置的值与预期的原值相同则更新,否则不更新,这是一种无锁的操作
        2. 使用synchorinized,ConcurrentHashMap的每个桶(也即是哈希表种的链表或者红黑树),都可以当作一个锁,线程访问时,锁住这个桶,而不是锁住整个哈希表
    • 6种状态:
    • synchronized:
      • 作用:把证同一时刻只能有一个线程来执行该段代码,保证线程的同步
      • 原理:使用了JVM种的监视器锁monitor,每个对象都有一个内治所,当线程调用synchronized方法时,就获得了这个锁,
      • JDK1.6 之后的优化:引入了偏向锁、和轻量级锁,逐步升级到重量级锁
      • synchronized和volatile的区别:volatile只能保证线程的可见性,当一个线程修改了这个变量的值,其他线程立刻可见这个修改
      • 并发编程的三个重要特性:原子性(全做or全部做),可见性(共享变量可见),有序性(按照先后顺序)
      • ThreadLocal原理:维护一个ThreadLocalMap,key为ThreadLocal对,值为变量
        • 内存泄漏:key是弱引用,value是强引用,所以外部没有使用强引用时,ThreadLocal会被回收,但是value会继续使用内存,可以使用ThreadLocal.remove()来解决这个问题
  • JUC:
    • Java Util Concurrent是并发编程的一个工具包,常用:
      • Executor框架
      • ConcurrentHashMap并发集合
      • CountDownLatch同步工具
      • Locks 比synchronized更灵活的锁机制
      • 原子变量:AtomicInteger
      • 并发工具类:ForkJoinPool
    • volatile关键字:保证变量的可见性,要求每次使用它时都需要从主存中重新读取,但不能保证原子性
    • 单例模式示例代码:
      public class Singleton {
      
          private volatile static Singleton uniqueInstance;
      
          private Singleton() {
          }
      
          public  static Singleton getUniqueInstance() {
             //先判断对象是否已经实例过,没有实例化过才进入加锁代码
              if (uniqueInstance == null) {
                  //类对象加锁
                  synchronized (Singleton.class) {
                      if (uniqueInstance == null) {
                          uniqueInstance = new Singleton();
                      }
                  }
              }
              return uniqueInstance;
          }
      }
      

集合相关

Set

  • HashSet底层使用的是HashMap
  • LinkedHashSet通过LinkedHashMap实现的
  • TreeSet:红黑树(自平衡的排序二叉树)实现

List

ArrayList是动态数组,可以扩容、缩容,同时可以使用泛型来保证,线程不安全,适用于频繁的查找工作。可添加null值
Vector是老实现,底层使用Object [ ]存储,线程安全

Queue

Map

HashMap

JDK1.8之前由数组+链表组成。JDK1.8之后当链表长度大于阈值,会转化为红黑树(当前数组的长度小于64时,会先进行数组扩容,而不是转化为红黑树)
LinkedHashMap:继承HashMap,增加了 一条双向链表。
JDK1.7时,HashMap在多线程环境下,扩容操作使用的是头插法,导致链表中的节点指向错误的位置。JDK1.8使用的是为尾插法,但是有可能会出现数据覆盖的问题,并发环境下推荐使用ConcurrentHashMap

Queue

PriorityQueue

使用二叉堆实现,底层使用可变长的数据来存储数据,非线程安全,不知处NULL值和不可排序的对象,默认是小顶堆

BlockingQueue

阻塞队列