八股文之java基础

jdk9中对字符串进行了一个什么优化?

jdk9之前 字符串的拼接通常都是使用+进行拼接 但是+的实现我们是基于stringbuilder进行的

这个过程通常比较低效 包含了创建stringbuilder对象 通过append方法去将stringbuilder对象进行拼接 最后使用tostring方法去转换成最终的字符串

在jdk9之后引入了stringconcatfactory 这个被推出的主要目标就是提供一个灵活且高效的方式去拼接字符串 代替之前的stringbuilder或者stringbuffer的静态编译方法

stringconcatfactory是基于invokedynamic指令来实现的

他是java7引入发一种动态类型的指令 允许jvm在运行的时候动态解析和调用方法

也就是说可以使用invokedynamic的特性 将字符串的拼接从编译时候推迟到运行时

这就使得 jvm可以在运行的时候根据实际的场景 去选择最优的拼接策略 可能是使用stringbuilder 也可能是stringbuffer或者是其他更高效的方法

stream的并行流一定会比串行流更快吗?

不一定

stream的底层使用的是forkjoin进行并发处理 但是这并不代表的并行流会快 因为他受到一些因素的影响

线程管理的开销:并行流使用了多线程 使用了多线程增加了线程管理和任务分配的开销

任务分割:并行流的性能依赖于任务能有效的分割和分配 如果任务分配不均衡一些线程可能会空闲或者等待 影响性能

线程争用:stream使用forkjoinpool 如果系统中有其他并行的任务 这些任务会争用线程资源 可能会导致性能下降

数据依赖性:并行流适用于没有数据依赖性的操作 如果操作之间有依赖关系 并行流无法有效的提高性能 还有可能会导致错误

环境配置:机器的硬件配置eg(cpu核数)和当前系统的负载也会影响并行流的性能 如果cpu核心数较少或者是负载较高 并行流的性能可能不如串行流

在单核的cpu的情况下 并行流的迭效率低于穿行流

在多核cpu的情况下 并行流的迭代效率高于串行流 但是如果元素数量比较少的情况下 常规迭代的性能反而较好

怎么去修改一个类中的private修饰的string参数的值

面试官问这个问题要么是在问反射 要么就是在挖坑

string对象一旦被创建就确定了 他的内容不会被修改 看似修改了string的值实际上就是创建了一个新的string对象

但是如果创建一个新的string对象也算是修改了的话 他有几种方式

一:使用setter方法

public class MyClass {
    private String myString;

    public void setMyString(String value) {
        this.myString = value;
    }
}

// 使用
MyClass obj = new MyClass();
obj.setMyString("new value");

二:使用反射

如果没有setter方法可以使用 可以使用反射 但是这种方法破坏了封装性 增加了代码的复杂性和出错的可能性 并且性能并不好

import java.lang.reflect.Field;

public class MyClass {
    private String myString = "initial value";
}

// 使用反射修改
MyClass obj = new MyClass();
try {
    Field field = MyClass.class.getDeclaredField("myString");
    field.setAccessible(true); // 使得private字段可访问
    field.set(obj, "new value");
} catch (NoSuchFieldException | IllegalAccessException e) {
    e.printStackTrace();
}

bigdecimal和long表示金额哪个更合适 怎么去选择

大家都知道float和double不能用来表示金额 会存在精度的丢失

要表示金额的话可以使用long或者是bigdecimal

1.单位为分 数据库中使用bigint代码中用long

2.单位为元 数据库中用decimal 代码中用bigdecimal

首先是bigdecimal是java中用于精确计算的类 特别适用于需要高精度数值计算的场景 如 金融 计量和工程领域 其特点:

精确度高:bigdecimal可以表示非常大或非常精确的小数 而不会出现浮点数那样的舍入误差

灵活的数学计算:他提供了各种方法进行精确的算数运算 包括加减乘除和四舍五入

控制舍入行为: 在进行数学计算的时候 你可以指定舍入模式 这对于金融计算非常重要

所以,BigDecimal的适用场景是需要高精度计算的金融应用,如货币计算、利率计算等。比如我们的结算系统、支付系统、账单系统等,都是用BigDecimal的。

其次,再说Long,long 是 Java 的一种基本数据类型,用于表示没有小数部分的整数。其特点如下:

●性能高:作为基本数据类型,long 在处理速度上比 BigDecimal 快很多。

●容量限制:long 可以表示的最大值为 (2^{63}-1),最小值为 (-2^{63})。这在大多数应用程序中已经足够,但在表示非常大的数或需要小数的计算中则不适用。

●不适合精确的小数运算:long 无法处理小数,如果需要代表金额中的小数部分(如厘),则需要自行管理这一部分。

所以,Long的适用场景是适合于不涉及小数计算的大整数运算,如某些计数应用或者金额以整数形式表示。比如我们的额度系统、积分系统等。

我一笔账单,有两笔订单,金额都是1元,存储的时候按照分存储,即100分,然后我的服务费费率是0.004。

如果是以分为单位,long存储和表示的话,那么两笔订单分开算费率的话:100*0.004 = 0.4 ,四舍五入 0, 两笔加在一起,收入的费率就是0分。

但是如说是以元为单位,bigdecimal存储和表示的话,那么两笔订单分开算费率的话:1*0.004 = 0.004 , 两笔加在一起0.008,当我要结算的时候,再做四舍五入就是0.01元,即1分钱。

所以,因为long在计算和存储的过程中都会丢失掉小数部分,那就会导致每一次都被迫需要四舍五入。而decimal完全可以保留过程中的数据,再最终需要的时候做一次整体的四舍五入,这样结果就会更加精确!

所以,如果你的应用需要处理小数点后的精确计算(如金融计算中常见的多位小数),则应选择 BigDecimal。

如果你的应用对性能要求极高,并且没有乘除类运算,不需要很小的精度时,那么使用 long 可能更合适。

总结来说,对于绝大多数涉及货币计算的应用,推荐使用 BigDecimal,因为它提供了必要的精度和灵活性,尽管牺牲了一些性能。如果确定不需要处理小数,并且对执行速度有极端要求,使用 long 可能更适合。

有了equals为啥需要hashCode方法?

在java中equals和hashcode通常是成对出现的 他们在 使用hash机制的数据结构中非常的重要 例如

hashmap hashset hashtable

equals是用来判断两个对象是否相等

hashcode是生成对象的哈希码 返回值是一个整数 用于确定对象在哈希表中的位置

为什么需要hashcode是因为在这种数据结构中 想要把一个对象放进去 就需要定位到他应该放在哪个桶里面 而这个桶的位置 就需要通过一个整数来获取 然后再对桶的长度进行取模

那么如何快速获取这个对象有关的整数 就是需要hashcode方法 hashcode的结果和对象的内容息息相关 那么也就意味着 如果两个对象通过equals方法比较是相等的那么他们的hashcode也是相等的

那么在一个对象中 定义了equals方法 还需要同时定义他的hashcode方法这样就可以在hashmap和hashtable中存放的时候 才能快速定位到位置

所以 基于两方面进行考虑 一方面是效率 hashcode()方法提供了一种快速计算对象hash值的方式 这些哈希值用于确定对象在哈希表中的位置 这意味着可以快速定位到对象应该存储在哪一个位置或者从哪个位置检索 限制提高了查找的效率

另一方面 可以和equals做协同来保证数据的一致性和准确性 根据java规范 如果两个对象通过equals方法比较时是相等的 那么这两个对象的hashcode方法必须返回相同的整数值 如果违反了这一规则 将导致哈希表等数据结构无法正确的处理对象 从而导致数据丢失和检索失败

java中的static都能用来修饰什么?

static是一个非常重要的修饰符 他通常被用来修饰变量 代码块 方法 以及类

1.静态变量

定义:静态变量属于类本身而不是类的任何特定实例(new出来的对象)

特点:

所有 的实例共享同一静态变量

在类加载到内存的时候就被初始化了 而不是在创建对象的时候

常用于管理类的全局状态或作为常量仓库

2.静态方法

定义:静态方法同样属于类 而非类的实例

特点:

可以在不创建类的实例的时候就调用

不能访问类的实例变量或实例方法 他们只能访问其他 的 静态成员

常用于工具类的方法

3.静态代码块

定义:用于初始化类的静态变量

特点

当类被java虚拟机加载并初始化时执行

通常用于执行静态变量的复杂初始化

4.静态内部类

定义:在一个类的内部定义的静态类

特点:

可以不依赖于外部类的实例而独立存在

可以访问外部类的所有静态成员 但不能直接访问外部类的实例成员

常用于当内部类的行为不依赖于外部类的实例时

使用static修饰符的好处就是可以减少内存的使用 (共享静态变量 而不是给每一个变量创建一个副本)提供一个全局发访问点 (如静态方法和变量)以及无需实例化类即可使用其中的变量和方法

为什么Java中的main方法必须是public static void的?

常见的形式

public static void main(String[] args) {

}

对于main方法来说我们需要jvm直接去调用它 所以需要设置为public

static是静态修饰符 被他修饰的方法我们称之为静态方法 静态方法有一个特点就是独立于该类的任何对象 它不依赖于类的特定的实例 被类的所有实例共享 只要这个类被加载 java虚拟机就能根据类名在运行时数据区的方法区内找到他们

对于main方法来说 他的调用过程是经历了类加载 连接和初始化的 但是并没有被实例化过 这个时候如果想要调用一个类中的方法这个方法必须是静态方法 否则不能调用

像一般c或c++语言的话是以int为main函数返回值的编程语言 这个返回值是在程序退出时的exit code 一般被命令解释器或者其他的外部程序调用已确定流程是否完成 一般正常情况下用0返回 为0为正常退出

但是在java中这个退出的过程是由jvm进行控制的一般在发生以下两种情况的时候程序会中止所有的i行为并退出

1.除了守护线程外 所有线程都全部中止

2.某个线程调用了runtime类或者system类的exit方法 并且安全管理器并不禁止exit操作

当第二种情况发生的时候jvm不会管main方法有没有执行完的 他会终止所有的行为并且退出 这时候main方法有没有返回值没有任何意义 所以是void

string[ ]字符串数组是由于java是由命令行来接收一个参数进行传入的 因为命令行参数最终都是以字符串的形式传递的 并且有的时候命令行的参数不止一个 所有就有可能传递多个参数

为什么不建议使用异常控制业务流程

在effective java中作者提出过 不建议使用异常来控制业务流程

eg:解决幂等问题的时候 先插入 然后再捕获唯一性约束冲突异常 在反查 返回幂等

这样做非常不建议:

1.存在性能问题 在java中异常的生成和处理是非常昂贵的 因为他设计到填充栈跟踪信息频繁的抛出和捕获异常会导致性能下降

2.异常的职责就不是干这个:java中的异常被定义为处理一些非正常情况的 它的使用应该是比较谨慎的 异常应该用于处理非预期的错误情况 而不是利用它来控制正常的业务流程 这样会使代码的意图变得不清晰 增加了理解和维护代码的难度

3.异常的捕获会影响事务的回滚:本身的这个方法如果还有其他的数据库操作逻辑 或者方法外嵌套了一层方法 那么就可能由于异常导致不能回滚

4.过度依赖数据库异常 :这里过度依赖了duplicatekeyexception 万一哪天这个异常发生了变化 比如版本升级看 或者底层数据库变了 不再抛出这个异常 了那么这个代码就会失去作用 可能导致意想不到的问题

良好的api应该使清晰的表达意图 如果api使用异常来表示常规的业务流程控制可能会误导api的使用者 使他们误解api的真正的用途

前面提到的幂等问题,要解决幂等问题,应该是先查,再改。如果为了防止并发,应该是一锁、二判、三更新。

为什么这段代码在JDK不同版本中结果不同

一些在线的Java代码执行工具测试,如:在线运行Java 。)

以下代码中,在JDK 1.8中,JDK 11及以上版本中执行后结果不是一样的。

String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);

你会发现,在JDK 1.8中,以上代码得到的结果是true,而JDK 11及以上的版本中结果却是false。

那么,再稍作修改呢?在目前的所有JDK版本中,执行以下代码:

String s3 = new String("3") + new String("3");// ①
s3.intern();// ②
String s4 = "33";
System.out.println(s3 == s4);// ③

得到的结果也是true,你知道为什么嘛?

看这篇文章之前先确保自己了解了intern的原理!!!

出现上述现象,肯定是因为在JDK 11 及以上的版本中,"11"这个字面量已经被提前存入字符串池了。那什么时候存进去的呢?(这个问题,全网应该没人提过)

经过我七七四十九天的研究,终于发现了端倪,就在以下代码中:Source.java

public enum Source {
    /** 1.0 had no inner classes, and so could not pass the JCK. */
    // public static final Source JDK1_0 =              new Source("1.0");

    /** 1.1 did not have strictfp, and so could not pass the JCK. */
    // public static final Source JDK1_1 =              new Source("1.1");

    /** 1.2 introduced strictfp. */
    JDK1_2("1.2"),

    /** 1.3 is the same language as 1.2. */
    JDK1_3("1.3"),

    /** 1.4 introduced assert. */
    JDK1_4("1.4"),

    /** 1.5 introduced generics, attributes, foreach, boxing, static import,
     *  covariant return, enums, varargs, et al. */
    JDK5("5"),

    /** 1.6 reports encoding problems as errors instead of warnings. */
    JDK6("6"),

    /** 1.7 introduced try-with-resources, multi-catch, string switch, etc. */
    JDK7("7"),

    /** 1.8 lambda expressions and default methods. */
    JDK8("8"),

    /** 1.9 modularity. */
    JDK9("9"),

    /** 1.10 local-variable type inference (var). */
    JDK10("10"),

    /** 1.11 covers the to be determined language features that will be added in JDK 11. */
    JDK11("11");
}

看到了么,xdm,在JDK 11 的源码中,定义了"11"这个字面量,那么他会提前进入到字符串池中,那么后续的intern的过程就会直接从字符串池中获取到这个字符串引用。

按照这个思路,大家可以在JDK 11中执行以下代码:

String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);


String s3 = new String("1") + new String("2");
s3.intern();
String s4 = "12";
System.out.println(s3 == s4);

得到的结果就是false和true。

或者我是在JDK 21中分别执行了以下代码:

String s3 = new String("2") + new String("1");
s3.intern();
String s4 = "21";
System.out.println(s3 == s4);


String s3 = new String("2") + new String("2");
s3.intern();
String s4 = "22";
System.out.println(s3 == s4);

得到的结果就也是false和true。

为什么建议自定义一个无参的构造函数?

java中的构造函数分为有参和无参

两种构造函数都是为了对象的初始化的 无参的构造函数是为了给对象一个默认值 有参构造函数就是根据我们的参数进行初始化

如果没有显示定义任何构造函数 会自动添加一个无参的构造函数 但是如果定义过构造函数 那么就不会默认添加了

定义一个无参的构造函数 通常是认为在java编程中的好习惯 如果我们没有定义jdk会帮我们生成一个 但是如果我们定义了一个有参的构造函数 那么就不会帮我们生成无参构造函数了 没有无参构造函数会带来一些列问题

1.反射及序列化要求

在使用java反射或者序列化的时候经常调用类的无参构造函数进行对象的创建

2.兼容性和可推广性

许多java框架和库 如spring hibernate jackson 在进行对象的创建和初始化时 依赖于类的无参构造器如果没有定义无参构造器,这些框架可能无法正常工作。

  1. JavaBean规范

根据JavaBean规范,一个标准的JavaBean必须拥有一个公共的无参构造器。这使得JavaBean可以被实例化,并且其属性可以通过反射机制被外部访问和修改。

  1. 子类构造器的默认行为

在Java中,子类构造器默认会调用父类的无参构造器。如果父类没有定义无参构造器,而子类又没有显式调用父类的其他构造器,这将导致编译错误。

final、finally、finalize有什么区别

final、finally和finalize是Java中的三个不同的概念。

●final:用于声明变量、方法或类,使之不可变、不可重写或不可继承。

●finally:是异常处理的一部分,用于确保代码块(通常用于资源清理)总是执行。

●finalize:是Object类的一个方法,用于在对象被垃圾回收前执行清理操作,但通常不推荐使用。

finally是一个用于异常处理,它和try、catch块一起使用。无论是否捕获或处理异常,finally块中的代码总是执行(程序正常执行的情况)。通常用于关闭资源,如输入/输出流、数据库连接等。

finalize是Object类的一个方法,用于垃圾收集过程中的资源回收。在对象被垃圾收集器回收之前,finalize方法会被调用,用于执行清理操作(例如释放资源)。但是,不推荐依赖finalize方法进行资源清理,因为它的调用时机不确定且不可靠。

try中return A,catch中return B,finally中return C,最终返回值是什么?

最终的返回值将会是C!

因为finally块总是在try和catch块之后执行,无论是否有异常发生。如果finally块中有一个return语句,它将覆盖try块和catch块中的任何return语句。

//无异常情况
public static String getValue(){
    try{
        return "A";
    }catch (Exception e){
        return "B";
    }finally {
        return "C";
    }
}

//有异常情况
public static String getValue(){
    try{
        System.out.println(1/0);
        return "A";
    }catch (Exception e){
        return "B";
    }finally {
        return "C";
    }
}

所以在这种情况下,无论try和catch块的执行情况如何,finally块中的return C;总是最后执行的语句,并且其返回值将是整个代码块的返回值。

这个问题还有一个兄弟问题,那就是如下代码得到的结果是什么:

public static void getValue() {

    int i = 0;

    try {
        i = 1;
    } catch (Exception e) {
        i = 2;
    } finally {
        i = 3;
    }
    System.out.println(i);
}

原理和上面的是一样的,最终输出内容为3。

finally和return的关系

很多时候,我们的一个方法会通过return返回一个值,那么如以下代码:

public static int getValue() {

    int i = 1;

    try {
         i++;
         return i;
    } catch (Exception e) {
        i = 66;
    } finally {
        i = 100;
    }

    return i;
}

这个代码得到的结果是2,try-catch-finally的执行顺序是try->finally或者try-catch-finally,然后在执行每一个代码块的过程中,如果遇到return那么就会把当前的结果暂存,然后再执行后面的代码块,然后再把之前暂存的结果返回回去。

所以以上代码,会先把i++即2的结果暂存,然后执行i=100,接着再把2返回。

但是,在执行后续的代码块过程中,如果遇到了新的return,那么之前的暂存结果就会被覆盖。如:

public static int getValue() {

    int i = 1;

    try {
         i++;
         return i;
    } catch (Exception e) {
        i = 66;
    } finally {
        i = 100;
        return i;
    }
}

以上代码方法得到的结果是100,是因为在finally中遇到了一个新的return,就会把之前的结果给覆盖掉。

如果代码出现异常也同理:

public static int getValue() {

    int i = 1;

    try {
        i++;
        System.out.println(1 / 0);
        return i;
    } catch (Exception e) {
        i = 66;
        return i;
    } finally {
        i = 100;
        return i;
    }
}

在try中出现一个异常之后,会执行catch,在执行finally,最终得到100。如果没有finally:

public static int getValue() {

    int i = 1;

    try {
        i++;
        System.out.println(1 / 0);
        return i;
    } catch (Exception e) {
        i = 66;
        return i;
    } 

那么得到的结果将是66。

所以,如果finally块中有return语句,则其返回值将是整个try-catch-finally结构的返回值。如果finally块中没有return语句,则try或catch块中的return语句(取决于哪个执行了)将确定最终的返回值。

为什么建议多用组合少用继承?

java的复用有继承组合代理三种具体的表现形式

继承相当于一个白盒复用 由于父类的内部细节都对子类是可见的 如果基类的代码发生改变 那派生类的实现也随之改变 这样就导致了子类行为的不可预知性

继承,在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。)

组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是黑盒式代码复用。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法)

组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。

相信很多人都知道面向对象中有一个比较重要的原则『多用组合、少用继承』或者说『组合优于继承』。从前面的介绍已经优缺点对比中也可以看出,组合确实比继承更加灵活,也更有助于代码维护。

所以,建议在同样可行的情况下,优先使用组合而不是继承。因为组合更安全,更简单,更灵活,更高效。

注意,并不是说继承就一点用都没有了,前面说的是【在同样可行的情况下】。有一些场景还是需要使用继承的,或者是更适合使用继承。

继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该好好考虑是否需要继承。《Java编程思想》

只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在is-a关系的时候,类B才应该继承类A。《Effective Java》

Java中Timer实现定时调度的原理是什么?

java中的timer是一个定时的调度器 用于在指定的时间点执行任务

public class Timer {
    /**
     * The timer task queue.  This data structure is shared with the timer
     * thread.  The timer produces tasks, via its various schedule calls,
     * and the timer thread consumes, executing timer tasks as appropriate,
     * and removing them from the queue when they're obsolete.
     */
    private final TaskQueue queue = new TaskQueue();

    /**
     * The timer thread.
     */
    private final TimerThread thread = new TimerThread(queue);
}

taskqueue 一个任务队列 用于存储已计划的定时任务 任务队列按照任务的执行时间进行排序 确保最早执行的任务排在队列前面 在队列中的任务可能是一次性的也可能是周期性的

timerthread timer内部的后台线程 他负责扫描task queue中的任务 检查任务的执行时间 在执行时间到达执行任务的run()方法 timerthread是一个守护线程 当所有的非守护线程全部结束的时候 他也随之中止

任务的定时调度的核心代码就在TimerThread中:

class TimerThread extends Thread {
   
    boolean newTasksMayBeScheduled = true;

    /**
     * 存储 TimerTask 的队列
     */
    private TaskQueue queue;

    TimerThread(TaskQueue queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            mainLoop();
        } finally {
            synchronized (queue) {
                newTasksMayBeScheduled = false;
                queue.clear(); 
            }
        }
    }

    /**
     * 主要的计时器循环。 
     */
    private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized (queue) {
                    // 等待队列变为非空
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; // 队列为空,将永远保持为空;线程终止

                    // 队列非空;查看第一个事件并执行相应操作
                    long currentTime, executionTime;
                    task = queue.getMin();
                    synchronized (task.lock) {
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;  // 无需执行任何操作,再次轮询队列
                        }
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        if (taskFired = (executionTime <= currentTime)) {
                            if (task.period == 0) { // 非重复,移除
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { // 重复任务,重新安排
                                queue.rescheduleMin(
                                  task.period < 0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    if (!taskFired) // 任务尚未触发;等待
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)  // 任务触发;运行它,不持有锁
                    task.run();
            } catch (InterruptedException e) {
            }
        }
    }
}

可以看到,TimerThread的实际是在运行mainLoop方法,这个方法一进来就是一个while(true)的循环,他在循环中不断地从TaskQueue中取出第一个任务,然后判断他是否到达执行时间了,如果到了,就触发任务执行。否则就继续等一会再次执行。

不断地重复这个动作,从队列中取出第一个任务进行判断,执行。。。

这样只要有新的任务加入队列,就在队列中按照时间排序,然后唤醒timerThread重新检查队列进行执行就可以了。代码如下:

 private void sched(TimerTask task, long time, long period) {
        if (time < 0)
            throw new IllegalArgumentException("Illegal execution time.");

        // Constrain value of period sufficiently to prevent numeric
        // overflow while still being effectively infinitely large.
        if (Math.abs(period) > (Long.MAX_VALUE >> 1))
            period >>= 1;

        synchronized(queue) {
            if (!thread.newTasksMayBeScheduled)
                throw new IllegalStateException("Timer already cancelled.");

            synchronized(task.lock) {
                if (task.state != TimerTask.VIRGIN)
                    throw new IllegalStateException(
                        "Task already scheduled or cancelled");
                task.nextExecutionTime = time;
                task.period = period;
                task.state = TimerTask.SCHEDULED;
            }
        	//新任务入队列
            queue.add(task);
            //唤醒任务
            if (queue.getMin() == task)
                queue.notify();
        }
    }

优缺点

Timer 类用于实现定时任务,最大的好处就是他的实现非常简单,特别的轻量级,因为它是Java内置的,所以只需要简单调用就行了。

但是他并不是特别的解决定时任务的好的方案,因为他存在以下问题:

1、Timer内部是单线程执行任务的,如果某个任务执行时间较长,会影响后续任务的执行。

2、如果任务抛出未捕获异常,将导致整个 Timer 线程终止,影响其他任务的执行。

3、Timer 无法提供高精度的定时任务。因为系统调度和任务执行时间的不确定性,可能导致任务执行的时间不准确。

4、虽然可以使用 cancel 方法取消任务,但这仅仅是将任务标记为取消状态,仍然会在任务队列中占用位置,无法释放资源。这可能导致内存泄漏。

5、当有大量任务时,Timer 的性能可能受到影响,因为它在每次扫描任务队列时都要进行时间比较。

6、Timer执行任务完全基于JVM内存,一旦应用重启,那么队列中的任务就都没有了

什么是序列化与反序列化

在java中 我们创建的java对象都是存放在jvm的堆内存中的 当jvm处于运行状态的时候 这些对象才可能存在 一旦jvm停止运行这些对象的状态随之消失

但是在真实的应用场景中 我们要将这些对象持久化下来 就是通过序列化 将这些对象的状态保存为字节数组 并且可以在有需要的时候将他通过反序列化的方式转换为对象 对象序列化可以很容易的在jvm中的活动对象和字节数组之间进行转换

以下几个和序列化&反序列化有关的知识点大家可以重点关注一下:

1、在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。

2、通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化

3、虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)

4、序列化并不保存静态变量。

5、要想将父类对象也序列化,就需要让父类也实现Serializable 接口。

6、transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

7、服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

未实现Serializable,可以序列化吗?

如果使用Java原生的序列化机制(即通过 ObjectOutputStream 和 ObjectInputStream 类),则对象必须实现 Serializable 接口。如果对象没有实现这个接口,尝试原生序列化会抛出 NotSerializableException。

对于像Jackson、Gson这样的JSON序列化库或用于XML的库(如JAXB),对象不需要实现 Serializable 接口。这些库使用反射机制来访问对象的字段,并将它们转换成JSON或XML格式。在这种情况下,对象的序列化与 Serializable 接口无关。

String中intern的原理是什么?

1

字符串常量是什么时候进入到字符串常量池的?

2

String是如何实现不可变的?

我们都知道string是不可变的 那他是怎么实现的

1.string类首先是被设置为final类 意味着他不能被继承 那么他里面的方法就是没办法被覆盖的

2.被final修饰字符串内容的char[](从jdk1.9开始 char[]就变成了byte[])由于该数组被声明为final 一旦数组被初始化 就不能再指向其他数组

3.string类没有提供用于修改字符串内容的公共方法 eg 没有提供用于追加 删除 或修改字符的方法 如果要对字符串进行修改就会创建一个新的string对象

如果我们想要一个可修改的字符串,可以选择StringBuffer 或者 StringBuilder这两个代替String。

Arrays.sort是使用什么排序算法实现的?

array。sort是java中提供的对数组进行排序的方法 根据参数类型的不同 他提供了很多重载方法

对于常见的基本数据类型 int double char 就是采用了 双轴快速排序

对于对象数组的排序 支持归并排序 和timsort

TimSort 是一种混合排序算法,结合了归并排序(Merge Sort)和插入排序(Insertion Sort)的特点。

为什么JDK 9中把String的char[]改成了byte[]?

在Java 9之前,字符串内部是由字符数组char[] 来表示的。

由于Java内部使用UTF-16,每个char占据两个字节,即使某些字符可以用一个字节(LATIN-1)表示,但是也仍然会占用两个字节。所以,JDK 9就对他做了优化。

每当我们创建一个字符串时,如果它的所有字符都可以用单个字节(Latin-1)表示,那么将会在内部使用字节数组来保存一半所需的空间,但是如果有一个字符需要超过8位来表示,Java将继续使用UTF-16与字符数组。

ClassNotFoundException和NoClassDefFoundError的区别是什么?

classnotfoundexception是一个受检异常 它通常在运行的时候在类加载阶段尝试加载类的过程中 找不到类的定义时触发 通常是由class。forname()或者类加载器loadclass或者findsystemclass时,在类路径中没有找到指定名称的类时 会报出该异常 表示所需的类路径不存在 通常是由于类名拼写错误或缺少依赖导致的

NoClassDefFoundError是一个错误(error),它表示运行时尝试加载一个类的定义时,虽然找到了类文件,但在加载、解析或链接类的过程中发生了问题。这通常是由于依赖问题或类定义文件(.class文件)损坏导致的。也就是说这个类在编译时存在,运行时丢失了,就会导致这个异常。

在编译后会生成A.class和B.class,当我们删除A.class之后,单独运行B.class的时候,就会发生NoClassDefFoundErrorNoSuchMethodError

NoSuchMethodError表示方法找不到,他和NoClassDefFoundError类似,都是编译期找得到,运行期找不到了。

这种error发生在生产环境中是,通常来说大概率是发生了jar包冲突。

while(true)和for(;;)哪个性能好?

while(true)和for(;;)都是做无限循环的代码,他俩有啥区别呢?

关于这个问题,网上有很多讨论,说那么多没用,直接反编译,看看字节码有啥区别就行了。然后再通过javap对class文件进行反编译,然后我们就会发现,两个文件内容,一模一样!!!

可以看到,都是通过goto来干的,所以,这两者其实是没啥区别的。用哪个都行

有人愿意用while(true)因为他更清晰的看出来这里是个无限循环。有人愿意用for(;;),因为有些IDE对于while(true)会给出警告。至于你,爱用啥用啥。

char能存储中文吗?

在Java中,char类型是用来表示一个16位(2个字节)的Unicode字符,它可以存储任何Unicode字符集中的字符,当然也包括中文字符。

但是,有人说,Java中的char是没办法表示生僻字的,这么说其实有点绝对了。

因为Unicode字符集包含了几乎所有的字符,包括常见字符、生僻字、罕见字以及其他语言的字符。所以,用char类型其实是可以存储生僻字的。

但是,在处理生僻字时,需要确保Java源代码文件本身以及编译器和运行时环境都支持Unicode字符集。另外,如果在字符串中使用生僻字,也需要注意字符编码和字符串长度的问题。

还有一点需要注意,Unicode字符集的目标是覆盖世界上所有的字符。然而,由于生僻字的数量庞大且不断增长,Unicode字符集可能无法及时收录所有生僻字。这主要取决于Unicode标准的版本以及生僻字的使用频率和普及程度。

虽然Unicode字符集也在一直不断的迭代更新,但是对于一些非常罕见的生僻字,它们可能因为版本问题,或者时间问题,暂时不在Unicode字符集中。在这种情况下,可能就会无法表示。

什么是UUID,能保证唯一吗?

uuid全局唯一标识符 是指在一台机器上生成的数字 他的目标是保证对在同一时空中的所有机器都是唯一的

uuid的生成是基于一定的算法 通常使用的是随机数生成器或者基于时间戳的方式 生成的uuid由32位16进制数表示 一共有128位 (8444 12)一共32个字符

由于uuid是由mac地址时间戳 随机数等信息生成的 因此uuid具有极高的唯一性可以说几乎不可能重复 但是在实际实现过程中 uuid有多种实现的版本 他们的唯一性指标也不同

有基于时间的uuid v1 基于随机数的uuid v4

优缺点

UUID的优点就是他的性能比较高,不依赖网络,本地就可以生成,使用起来也比较简单。

但是他也有两个比较明显的缺点,那就是长度过长和没有任何含义。长度自然不必说,他有32位16进制数字。对于"550e8400-e29b-41d4-a716-446655440000"这个字符串来说,我想任何一个程序员都看不出其表达的含义。一旦使用它作为全局唯一标识,就意味着在日后的问题排查和开发调试过程中会遇到很大的困难。

各个版本实现

V1. 基于时间戳的UUID

基于时间的UUID通过计算当前时间戳、随机数和机器MAC地址得到。由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。

但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。如果应用只是在局域网中使用,也可以使用退化的算法,以IP地址来代替MAC地址。

V2. DCE(Distributed Computing Environment)安全的UUID

和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID,这个版本的UUID在实际中较少用到。

V3. 基于名称空间的UUID(MD5)

基于名称的UUID通过计算名称和名称空间的MD5散列值得到。

这个版本的UUID保证了:相同名称空间中不同名称生成的UUID的唯一性;不同名称空间中的UUID的唯一性;相同名称空间中相同名称的UUID重复生成得到的结果是相同的。

V4. 基于随机数的UUID

根据随机数,或者伪随机数生成UUID。该版本 UUID 采用随机数生成器生成,它可以保证生成的 UUID 具有极佳的唯一性。但是因为基于随机数的,所以,并不适合数据量特别大的场景。

V5. 基于名称空间的UUID(SHA1)

和版本3的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。

各个版本总结

可以简单总结一下,Version 1和Version 2 这两个版本的UUID,主要基于时间和MAC地址,所以比较适合应用于分布式计算环境下,具有高度唯一性。

Version 3和 Version 5 这两种UUID都是基于名称空间的,所以在一定范围内是唯一的,而且如果有需要生成重复UUID的场景的话,这两种是可以实现的。

Version 4 这种是最简单的,只是基于随机数生成的,但是也是最不靠谱的。适合数据量不是特别大的场景下

JDK新版本中都有哪些新特性?

jdk8推出了lambda表达式 stream optional 新的日期api

jdk9推出了模块化

jdk10推出了本地变量类型判断

本地变量类型判断:

在jdk10之前的版本 我们想要定义一个局部的变量 我们需要在赋值的左边提供一个显式类型 并在赋值的右边提供一个实现的类型

MyObject value = new MyObject();

在Java 10中,提供了本地变量类型推断的功能,可以通过var声明变量:

var value = new MyObject();

本地变量的类型引入var 关键字不需要显示的规范变量的类型

其实所谓的本地变量类型推断 也就是java10提供给开发者的语法糖

虽然我们在代码中进行了var定义 但是对于虚拟机来说他是不认识这var的 在java文件编译成class文件的过程中 会进行一个解糖 使用变成真正的类型

jdk12增加了switch表示式

在JDK 12中引入了Switch表达式作为预览特性。并在Java 13中修改了这个特性,引入了yield语句,用于返回值。

而在之后的Java 14中,这一功能正式作为标准功能提供出来。

在以前,我们想要在switch中返回内容,还是比较麻烦的,一般语法如下:

int i;
switch (x) {
    case "1":
        i=1;
        break;
    case "2":
        i=2;
        break;
    default:
        i = x.length();
        break;
}

在JDK13中使用以下语法:

int i = switch (x) {
    case "1" -> 1;
    case "2" -> 2;
    default -> {
        int len = args[1].length();
        yield len;
    }
};

或者

int i = switch (x) {
    case "1": yield 1;
    case "2": yield 2;
    default: {
        int len = args[1].length();
        yield len;
    }
};

在这之后,switch中就多了一个关键字用于跳出switch块了,那就是yield,他用于返回一个值。

和return的区别在于:return会直接跳出当前循环或者方法,而yield只会跳出当前switch块。

jdk13增加了text block

jdk13中提供了一个text blocks 的预览特性 并且在java14中提供了第二个版本的预览

text block文本块

我们之前从外部copy一段文本串到java中 会被自动转义

 <html>
  <body>
      <p>Hello, world</p>
  </body>
</html>

将其复制到Java的字符串中,会展示成以下内容:

"<html>\n" +
"    <body>\n" +
"        <p>Hello, world</p>\n" +
"    </body>\n" +
"</html>\n";

即被自动进行了转义,这样的字符串看起来不是很直观,在JDK 13中,就可以使用以下语法了:

"""
<html>
  <body>
      <p>Hello, world</p>
  </body>
</html>
""";

使用"""作为文本块的开始符合结束符,在其中就可以放置多行的字符串,不需要进行任何转义。看起来就十分清爽了。

如常见的SQL语句:

String query = """
    SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
    WHERE `CITY` = 'INDIANAPOLIS'
    ORDER BY `EMP_ID`, `LAST_NAME`;
""";

jdk14增加了records

Java 14 中便包含了一个新特性:EP 359: Records,

Records的目标是扩展Java语言语法,Records为声明类提供了一种紧凑的语法,用于创建一种类中是"字段,只是字段,除了字段什么都没有"的类。

通过对类做这样的声明,编译器可以通过自动创建所有方法并让所有字段参与hashCode()等方法。这是JDK 14中的一个预览特性。

使用record关键字可以定义一个记录:

record Person (String firstName, String lastName) {}

record 解决了使用类作为数据包装器的一个常见问题。纯数据类从几行代码显著地简化为一行代码。(详见:Java 14 发布了,不使用"class"也能定义类了?还顺手要干掉Lombok!http://www.hollischuang.com/archives/4548

jdk14增加了instance模式匹配

instanceof是Java中的一个关键字,我们在对类型做强制转换之前,会使用instanceof做一次判断,例如:

if (animal instanceof Cat) {
    Cat cat = (Cat) animal;
    cat.miaow();
} else if (animal instanceof Dog) {
    Dog dog = (Dog) animal;
    dog.bark();
}

Java 14带来了改进版的instanceof操作符,这意味着我们可以用更简洁的方式写出之前的代码例子:

if (animal instanceof Cat cat) {
    cat.miaow();
} else if(animal instanceof Dog dog) {
    dog.bark();
}

jdk15增加了封闭类

在java15之前 java认为代码重用始终是一个终极的目标所以一个类和接口都可以被任意的类实现或者继承 但是 在很多的场景中 这样做是容易造成错误的

例如,假设一个业务领域只适用于汽车和卡车,而不适用于摩托车。

在Java中创建Vehicle抽象类时,应该只允许Car和Truck类扩展它。

通过这种方式,我们希望确保在域内不会出现误用Vehicle抽象类的情况。

为了解决类似的问题,在Java 15中引入了一个新的特性------密闭。

public sealed interface Service permits Car, Truck {
}

想要定义一个密闭接口,可以将sealed修饰符应用到接口的声明中。然后,permit子句指定允许实现密闭接口的类:

public abstract sealed class Vehicle permits Car, Truck {
}

以上代码定义了一个密闭接口Service,它规定只能被Car和Truck两个类实现。

与接口类似,我们可以通过使用相同的sealed修饰符来定义密闭类:

通过密闭特性,我们定义出来的Vehicle类只能被Car和Truck继承。

jdk17扩展了switch模式匹配

jdk21增加了协程

现在JDK的最新版本是什么?

目前Java的发布周期是每半年发布一次,大概在每年的3月份和9月份都会发布新版本。

在2023年9月份的时候发布了JDK 21。

2024年3月19日,JDK22正式发布。根据正常的发布节奏,接下来的发布情况应该是:

2024-09 ------> JDK 23

2025-03 ------> JDK 24

2025-09 ------> JDK 25

2026-03 ------> JDK 26

在JDK 22及之前的版本中,最后一个LTS版本(Long Term Support)是JDK 21。

SimpleDateFormat是线程安全的吗?使用时应该注意什么?

在日常的开发中 我们经常会用到时间 我们有很多办法在java代码中获取时间 但是不同的方式获取到的时间的格式不相同 我们这时候就需要一个格式化工具 把时间显示成我们需要的格式

最常用的方式就是使用simpledateformat类 这看起来是一个功能比较简单的类 但是一旦使用不当也有可能导致很大的问题

在阿里巴巴使用手册中

也就是说SimpleDateFormat是非线程安全的,所以在多线程场景中,不能使用SimpleDateFormat作为共享变量。

因为在simpledateformat中的format方法在执行的过程中 会使用一个成员变量calendar来保存时间

如果我们在声明SimpleDateFormat的时候,使用的是static定义的。那么这个SimpleDateFormat就是一个共享变量,随之,SimpleDateFormat中的calendar也就可以被多个线程访问到。

假设线程1刚刚执行完calendar.setTime把时间设置成2018-11-11,还没等执行完,线程2又执行了calendar.setTime把时间改成了2018-12-12。这时候线程1继续往下执行,拿到的calendar.getTime得到的时间就是线程2改过之后的。

想要保证线程安全,要么就是不要把SDF设置成成员变量,只设置成局部变量就行了,要不然就是加锁避免并发,或者使用JDK 1.8中的DateTimeFormatter

SimpleDateFormat用法

1111111111

什么是AIO、BIO和NIO?

BIO (Blocking I/O):同步阻塞I/O,是JDK1.4之前的传统IO模型。 线程发起IO请求后,一直阻塞,直到缓冲区数据就绪后,再进入下一步操作。

NIO (Non-Blocking I/O):同步非阻塞IO,线程发起IO请求后,不需要阻塞,立即返回。用户线程不原地等待IO缓冲区,可以先做一些其他操作,只需要定时轮询检查IO缓冲区数据是否就绪即可。

AIO ( Asynchronous I/O):异步非阻塞I/O模型。线程发起IO请求后,不需要阻塞,立即返回,也不需要定时轮询检查结果,异步IO操作之后会回调通知调用方。

Java中BIO、NIO、AIO分别适用哪些场景?

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

相关推荐
Algorithm15769 分钟前
云原生相关的 Go 语言工程师技术路线(含博客网址导航)
开发语言·云原生·golang
shinelord明19 分钟前
【再谈设计模式】享元模式~对象共享的优化妙手
开发语言·数据结构·算法·设计模式·软件工程
新手小袁_J24 分钟前
JDK11下载安装和配置超详细过程
java·spring cloud·jdk·maven·mybatis·jdk11
呆呆小雅25 分钟前
C#关键字volatile
java·redis·c#
Monly2125 分钟前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat
boligongzhu26 分钟前
DALSA工业相机SDK二次开发(图像采集及保存)C#版
开发语言·c#·dalsa
Eric.Lee202126 分钟前
moviepy将图片序列制作成视频并加载字幕 - python 实现
开发语言·python·音视频·moviepy·字幕视频合成·图像制作为视频
Ttang2327 分钟前
Tomcat原理(6)——tomcat完整实现
java·tomcat
7yewh29 分钟前
嵌入式Linux QT+OpenCV基于人脸识别的考勤系统 项目
linux·开发语言·arm开发·驱动开发·qt·opencv·嵌入式linux
钱多多_qdd38 分钟前
spring cache源码解析(四)——从@EnableCaching开始来阅读源码
java·spring boot·spring