Java基础面试题小结

基础面试题

Java语言简介

Java是1995年由sun公司推出的一门高级语言,该语言具备如下特点:

  1. 简单易学,相较于C语言和C++,没有指针的概念,所以操作和使用是会相对容易一些。
  2. 平台无关性,即Java程序可以通过Java虚拟机在不同硬件不同操作系统上运行,而无需进行任何修改。
  3. 面向对象:面向对象是一种程序设计技术,以木匠工作为例,使用面向对象方式实现的木匠的工作关注重点永远是制作椅子,其次才是工具。而面向过程则优先关注制作工具。与C++不同的是,Java不支持多继承,取而代之的是更加简单的接口的概念。
  4. 编译与解释并存:与C++RustGo不同的是,java源代码运行时需要先编译称为字节码文件(.class),然后再通过解释器翻译成机器码运行。
  5. 可靠性:Java通过早期检测以及运行时检测消除了容易出错的情况。以及与C++不同的是,操作数组、字符串方式采用的是指针模型避免了重写内存或者损坏数据的问题。
  6. 安全性:Java适用于网络/分布式环境,为了达到这个目标,Java在防病毒,防篡改做出很大的努力。
  7. 支持网络编程,使用非常方便,也有不错的性能。
  8. 支持多线程编程。

Java的三种技术架构

Java有多种技术架构,但以下是其中三种常见的:

  1. Java SE(Java Standard Edition):Java标准版是Java平台的基础版本,用于开发和部署桌面、嵌入式和服务器应用程序。它包含了Java编程语言、Java虚拟机(JVM)、Java类库等核心组件。Java SE提供了广泛的功能和API,可以用于开发各种类型的应用程序,从简单的命令行工具到复杂的图形用户界面(GUI)应用。

  2. Java EE(Java Enterprise Edition):Java企业版是用于开发和部署企业级应用程序的Java平台。它建立在Java SE的基础上,提供了一组扩展和技术,用于构建分布式、可扩展和安全的企业应用。Java EE包括诸如Servlet、JavaServer Pages(JSP)、Enterprise JavaBeans(EJB)、Java Persistence API(JPA)等技术,还提供了支持事务管理、安全性、消息传递、Web服务等企业级功能的各种规范和API。

  3. Java ME(Java Micro Edition):Java微版是专门设计用于嵌入式设备和移动设备的Java平台。它针对资源受限的环境,如智能手机、个人数字助理(PDA)、嵌入式系统等,提供了一个轻量级的Java运行时环境。Java ME包括了一组精简的Java类库和API,使开发人员能够构建适用于小型设备的应用程序,如移动游戏、手机应用等。

这些Java技术架构可以根据应用程序的需求和目标进行选择和使用。Java SE适用于通用的应用程序开发,Java EE适用于构建大型企业级应用,而Java ME适用于嵌入式和移动设备的开发。

JVM简介

Java虚拟机(Java Virtual Machine,JVM)是Java平台重要组成部分,它是一个在计算机上运行Java字节码的虚拟计算机,它负责解释并执行编译后的Java字节码,并将其转换为底层操作系统能够理解的机器码。

JDK和JRE分别是什么

JDK(Java Development Kit)是Java开发工具包,包含了JRE所有的东西,所以作为开发人员,只需要安装JDK即可

JRE(Java Runtime Environment)是Java运行环境,包含运行所需要的类库以及JVM。你可能认为如果仅仅要运行Java程序,安装JRE即,但是某些web程序例如需要将JSP 转换为 Java servlet就需要jdk编译了,所以保守起见,无论运行还是开发,我们都建议在操作系统上安装jdk

什么字节码文件,字节码文件的优势是什么

首先我们了解一下从编写Java代码到Java代码被执行的过程:

  1. 开发人员编写java代码。
  2. 通过javac编译生成字节码即.class结尾的文件。
  3. 通过解释器翻译计算机可以理解的机器码,需要注意的是JIT解释器通过运行时编译,会将某些字节码对应的机器码保存下来,这就是HotSpotLazy Evaluation原则,将那些热点代码保存起来,执行次数越多速度越快(这里参照了CPU分支预测器的工作原理)
  4. CPU收到机器码执行该指令。

JDK9的AOT技术以及为什么不全部使用 AOT 呢?

JDK9的AOT(Ahead-of-Time)编译技术使得Java字节码可以填编译为本地机器码,相比于传统JIT编译来说在运行时将字节码转为机器码,这样做带来了两个好处

  1. 启动时间快,因为AOT的提前编译,使得运行时避免了一些编译的开销,所以启动的效率得以提高。
  2. 执行性能优化,因为AOT编译是在运行前针对Java程序进行全局优化,而不是仅仅针对运行时的热点代码,所以程序执行性能相比JIT更高。

同样的,它也带来如下几个缺点:

  1. Java是一门动态语言,需要动态加载类、反射、动态代码生成等特性,这些特性无法在编译器静态决定,需要运行时才能进行决策的,所以使用AOT这些特性就无法支持了。

  2. Java应用程序需要长期的运行,可能需要动态加载和卸载类,或者在运行时进行动态的优化,AOT的静态编译就无法满足这些特性。

  3. Java生态系统包含大量的第三方框架,如果使用AOT可能使得很多特性都无法支持,最典型就是Spring框架的AOP。

Java与C++的区别

  1. Java没有指针的概念,不能像C++一样直接操作内存,虽无法实现更接近底层的操作,但是使用起来更加安全。
  2. Java不支持多继承,但是可以通过多接口实现多继承。
  3. Java只支持方法重载,不像C++一样可以运算符重载。
  4. Java有自动内存管理垃圾回收机制(GC),无需像C++一样手动释放。

Java 有哪几种基本数据类型?float 和 double 的区别是?(校招面试题)

java 复制代码
基本类型 位数 字节 默认值
int		32    4     0
short   16    2     0
long    64    8     0L
byte    8     1     0
char    16    2   'uo000'
float   32    4    0f
double  64    8    0d
boolean  1        false

float为4个字节,double为8字节

Java有那些引用类型

  1. 类 class
  2. 接口 interface
  3. 数组 \[\]

自动类型转换和强制类型转换

通俗来说将精度小的赋值给精度大的Java会进行自动转换,反之就需要我们进行强制转换了。

精度自小向大有两种情况。我们先来看看第一种比较特殊的自动类型转换,char赋值给int类型会自动转换为int。因为char占两个字节,存放都是0到 65535以内的整数,所以赋值给4个字节的int就会发生自动类型转换。

复制代码
  char c = 'a';
  int num = c;

反之int转char就需要强制转换了

复制代码
 int num=1;
 char c=(char)num;

另一种情况大家就比较熟悉了,即精度小的转为精度大的发生自动类型转换,反之就是强制类型转换,如下图所示:

自动装箱和自动拆箱

将基本数据类型赋值给包装类就是装箱,如下所示:

java 复制代码
Integer i = 10;  //装箱 

这一点,我们可以通过查看字节码得以印证,可以看到底层就是通过valueOf实现装箱。

java 复制代码
public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 5 L0
    BIPUSH 10
    INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
    ASTORE 1
   L1
    LINENUMBER 6 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    LOCALVARIABLE i Ljava/lang/Integer; L1 L2 1
    MAXSTACK = 1
    MAXLOCALS = 2
}
复制代码
 Integer i = 10;
 int num = i;  //拆箱 

查看字节码,可以看到底层是通过Integer.intValue实现。

java 复制代码
 // access flags 0x9
  public static main([Ljava/lang/String;)V
   
   L1
    LINENUMBER 6 L1
    ALOAD 1
    INVOKEVIRTUAL java/lang/Integer.intValue ()I
    ISTORE 2
   L2
    LINENUMBER 7 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    LOCALVARIABLE i Ljava/lang/Integer; L1 L3 1
    LOCALVARIABLE num I L2 L3 2
    MAXSTACK = 1
    MAXLOCALS = 3
}

&和&&的区别是什么

前者进行逻辑与时不会因为左边的false发生短路,而后者会发生短路。

具体我们可以查看下面这个例子,&运算不会发生短路,所以第一个func()结果返回false,第二个func()还是会被执行。

java 复制代码
 public static void main(String[] args) {
        boolean b1 = func() & func();
    }

    private static boolean func() {
        System.out.println("调用了func");
        return false;
    }

从输出结果我们可以得以印证:

java 复制代码
调用了func
调用了func

同理我们再看看&&运算,因为第一次返回了false,所以&&运算符后面的func()就不会被执行。

java 复制代码
 public static void main(String[] args) {
        boolean b1 = func() && func();
    }

    private static boolean func() {
        System.out.println("调用了func");
        return false;
    }

输出结果如下:

java 复制代码
调用了func

switch 是否能作用在 byte/long/String 上?

到Java7开始byteString都可以,但是long还是不行的。这一点我们使用IDEA写一段代码就可以知道。

break、continue、return的区别

  1. break:结束当前循环体
  2. continue:跳过本次循环
  3. return 结束当前循环直接返回结果

最有效的计算2乘8是什么方式?

用左移运算符即 2<<3 ,这种写法在JDK的数据结构中经常可以见到,例如Arrays中的binarySearch0二分搜索,它获取中间索引时除2就是用右移运算符号。

java 复制代码
 private static int binarySearch0(long[] a, int fromIndex, int toIndex,
                                     long key) {
        int low = fromIndex;
        int high = toIndex - 1;

        while (low <= high) {
        	//右移运算实现除2操作
            int mid = (low + high) >>> 1;
            long midVal = a[mid];

            if (midVal < key)
                low = mid + 1;
            else if (midVal > key)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found.
    }

这几个自增、自减操作的结果是什么?

代码端1:

复制代码
int i  = 1;
i = i++;
System.out.println(i); 

答案:

java 复制代码
输出1 ,++在后,先赋值在运算

代码段2:

复制代码
		 int i = 1;
        int temp = i;
        i++;
        i = temp;
        System.out.println(i);

答案:

java 复制代码
输出1 temp=i;做的事拷贝i,而不是指向i的地址

代码段3 :

复制代码
int count = 0;
for(int i = 0;i < 100;i++){
    count = count++;
}
System.out.println("count = "+count); 

答案:

java 复制代码
比较容易错,先运算再自增,所以输出结果为0

代码段4:

复制代码
int autoAdd(int count){
    int temp = count;
    count = coutn + 1;
    return temp;
}

答案:

java 复制代码
如果count为0,那么最终结果也是返回0

面向对象

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

  1. ⾯向过程 :面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的一次调用就可以。
  2. ⾯向对象 :面向对象,把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事件在解决整个问题的过程所发生的行为。 目的是为了写出通用的代码,加强代码的重用,屏蔽差异性。

面向对象有哪些特性

  1. 封装
  2. 多态
  3. 继承

重载和重写的区别

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;

重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)

方法重载的规则:

  1. 方法名一致,参数列表中参数的顺序,类型,个数不同。
  2. 重载与方法的返回值无关,存在于父类和子类,同类中。
  3. 可以抛出不同的异常,可以有不同修饰符。

访问修饰符有那些?

  1. default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。
  2. private : 在同一类内可见。可以修饰变量、方法。注意:不能修饰类(外部类)
  3. public : 对所有类可见。可以修饰类、接口、变量、方法
  4. protected : 对同一包内的类和所有子类可见。可以修饰变量、方法。注意:不能修饰类(外部类)

this关键字的作用是什么?

this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针this 的用法在 Java 中大体可以分为 3 种:

  1. 普通的直接引用,this 相当于是指向当前对象本身

  2. 形参与成员变量名字重名,用 this 来区分:

    public Person(String name,int age){
    this.name=name;
    this.age=age;
    }

  3. 引用本类的构造函数

抽象类和接口的区别

  1. 接⼝的⽅法默认是 public ,所有⽅法在接⼝中不能有实现(Java 8 开始接⼝⽅法可以有默认实现),⽽抽象类可以有⾮抽象的⽅法。
  2. 接⼝中除了 staticfinal 变量,不能有其他变量,⽽抽象类中则不⼀定。
  3. ⼀个类可以实现多个接⼝,但只能实现⼀个抽象类。接⼝⾃⼰本身可以通过 extends 关键字扩展多个接⼝。
  4. 接⼝⽅法默认修饰符是 public ,抽象⽅法可以有 publicprotecteddefault 这些修饰符(抽象⽅法就是为了被重写所以不能使⽤ private 关键字修饰!)
  5. 从设计层⾯来说,抽象是对类的抽象,是⼀种模板设计,⽽接⼝是对⾏为的抽象,是⼀种⾏为的规范。

两个类继承抽象类,都使用super.method,多线程情况下会出现线程安全吗

先说答案,不会,我们可以通过代码的方式印证,首先给出父类代码:

java 复制代码
public abstract class Parent {

    private HashMap<String, String> map =null;
    //构造方法会初始化map
    public Parent() {
        map = new HashMap<>();
    }

    //往map里添加东西
    public void add(String key, String value) {
        map.put(key, value);
    }
    //获取map
    public HashMap<String, String> getMap() {
        return map;
    }
}

然后两个子类代码

java 复制代码
public class A extends Parent{
    public A(){
        super.add("1","A");
    }
}
java 复制代码
public class B extends Parent{
    public B(){
        super.add("2","B");
    }
}

使用多线程模式进行调试时可以发现每一个自类都会走到父类的构造方法初始化一个父类,这就意味着彼此的map都是独立的,所以不存在线程安全问题。

可以看到A类的map地址为704。

B类的map地址为711。

深拷贝和浅拷贝了解嘛

  1. 浅拷贝:仅拷贝被拷贝对象的成员变量的值,也就是基本数据类型变量的值,和引用数据类型变量的地址值,而对于引用类型变量指向的堆中的对象不会拷贝。
  2. 深拷贝:完全拷贝一个对象,拷贝被拷贝对象的成员变量的值,堆中的对象也会拷贝一份。

创建Java对象的方式有哪几种?

  1. new
  2. 反射
  3. clone
  4. 序列化

什么是序列化?什么又是反序列化

序列化即将对象转成二进制流进行传输,而反序列化则是将二进制流转为对象

成员变量和局部变量有哪些区别?

  1. 从语法上分析:成员变量:可被访问修饰符以及static修饰。局部变量:不可被访问修饰符以及static等修饰。

  2. 从存储位置:成员变量在堆内存,局部变量是在栈内存。

从生存时间来看:成员变量若为static则随着类存在而存在,若无static则随着对象存在而存在。局部变量则随着方法调用结束。

从是否有默认值的角度来看:成员变量有默认值,局部变量没有默认值一说。

静态变量有什么作用

  1. 可被多个类共享,无论创建多少个类,使用static修饰的变量永远只有这一个。
  2. static再加一个final,这个变量就相当于一个常量,所以我们使用static关键字就必须考虑线程安全问题。

字符型常量和字符串常量的区别

  1. 字符类型在Java中占用两个字节,字符串若干个字节(你可以理解为末尾有个\0)
  2. 字符用单引号,字符串用双引号。

进制运算

计算机中进制转换的概念

在计算机中数据都是以2进制存储的,但为了更方便的表述数据,又出现了八进制十六进制

十进制如何转二进制

我们已十进制6转为二进制的运算为例,计算过程为:

  1. 首先是6除以2,得数3,余数0,2继续向下运算。
  2. 然后3除以2,得数1,余数也是1,得数1继续向下运算。
  3. 最后是1除以2,得数0,余数1,因为得数为0所以,停止运算。

最终结果自下向上拼接,得110,具体过程参见下图。

二进制如何转十进制

我们还是以上文得到的二进制110,来显示转为10进制的过程:

  1. 高位1*2的2次方。
  2. 中间次方递减,1*2的1次方。
  3. 最后1*2的0次方。
  4. 3者相加,得出6。

输出结果如下:

其他进制间的转换

其他进制间的转换,我们以2进制转为16进制为例,我们都知道16进制可以表示1-16之间的数据,而2进制4位才能表示16以内的数字。所以以二进制01011010为例,以4位为代表一个16进制。所以我们会将数字切割为:0101 1010。

随后0101转为10进制即5,而1010转为10用16进制表示即A。最终结果为5A。

同理2进制转8进制也是一样的,2进制的3位看作一个8进制数。

而任何进制转2进制也比较容易,只需转为10进制再转2进制即可,这里就不多做赘述了。

负数的二进制表现形式

我们还是以2进制6为例,它的2进制表示如下:

为了得到-6的2进制的表示形式,我们首先将6的2进制取反。

最后再加1,所以-6的2进制表示如下,这就是为什么正数高位为0,负数高位为1的原因。

Java基本类型相关问题

基本类型和包装类型的区别

  1. 包装类型成员变量默认为null,成员变量默认为该类型默认值,例如int成员变量默认为0
  2. 基本类型的局部变量会被存放在虚拟机栈局部变量表中,而非static的基本类型成员变量都是存放在堆中。而包装类型在HotSpot 逃逸分析发现并没有逃逸的外部时会避免分配在堆上,其他情况都会分配上堆区。
  3. 包装类型可以作为泛型,基本类型不可。

包装类型的缓存机制

Java会对4种基本整数类型(short、 int、 long、 byte)设置缓存数据,如下Integer 源码所示,可以看到我们尝试使用valueOf生成整型包装类时,如果传入的数值在IntegerCache以内则会从缓存中返回,反之则是创建一个全新的整型包装类对象。

复制代码
public static Integer valueOf(int i) {
		//如果在缓存范围以内,则
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

所以,如果我们在缓存范围内用valueOf,用==比较是会返回true的。

java 复制代码
public static void main(String[] args) {
        Integer i = Integer.valueOf(1);
        Integer i2 = Integer.valueOf(1);
        System.out.println(i == i2);
    }

输出结果:

java 复制代码
true

一旦超过整型包装类的缓存范围(默认为127),因为生成的对象并非来缓存,所以用==比较结果为false。

java 复制代码
 public static void main(String[] args) {
        Integer i = Integer.valueOf(128);
        Integer i2 = Integer.valueOf(128);
        System.out.println(i == i2);
    }

输出结果:

java 复制代码
false

所以,一般情况下,我们建议包装类的比较一律使用equals,从下面的源码我们就可以看出,包装类的equals被重写,比较的结果是获取value值进行==比较的。

java 复制代码
public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

修改后的代码如下:

java 复制代码
 public static void main(String[] args) {
        Integer i = Integer.valueOf(128);
        Integer i2 = Integer.valueOf(128);
        System.out.println(i.equals(i2));
    }

输出结果也会true了:

java 复制代码
true

注意:float和double没有实现缓存机制,原因也很简单,对于小数而言即使是一个很小的区间,要保存的小数也有很多,如果为每一个小数都设置缓存,会占用大量内存空间,所以设计者就没有考虑对这些高精度小数做缓存。

复制代码
 @Test
    public void doubleTest() {
        //        float double 没有实现缓存机制
        Double d = 1.2;
        Double d2 = 1.2;
        System.out.println(d == d2);
    }

自动装箱和自动拆箱

如下所示,将基本类型赋值给包装类型会触发自动装箱,调用valueOf返回一个对象实例。而自动拆箱则是调用xxxValue将包装类的值取出赋值给基本类型。

下面这段代码就是自动装箱:

java 复制代码
  Integer i = 10;

查看字节码,可以看到装箱操作相当于调用valueOf从返回一个对象实例。

java 复制代码
字节码核心代码:INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;

拆箱代码如下所示:

java 复制代码
 Integer i = 10;
int i2 = i;

自动拆箱,查看字节码相当于调用了intValue,这一点我们查看字节码文件也可以看出:

java 复制代码
 INVOKEVIRTUAL java/lang/Integer.intValue ()I

包装类的比较

如下代码i是通过装箱通过valueOf从缓存中取得,而i2则是自己从堆区创建的一个对象,所以两者返回false,要想比较数值必须使用equals

复制代码
@Test
    public void integerQuestion() {
        Integer i = 10;//相当于Integer.valueOf(10) 从缓存中拿值
        Integer i2 = new Integer(10); // 创建一个新的对象,所以两者值不相等
        System.out.println(i == i2);//false
        System.out.println(i.equals(i2));//true
    }

这个比较器也是同理,因为new Integer做的是在堆区创建一个对象,==比较的是两个引用的地址,所以返回1

复制代码
 @Test
    public void BoxedCompareTest() {
        Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
        int compare = naturalOrder.compare(new Integer(12), new Integer(12));
        System.out.println(compare);//返回1 因为integer new出来的对象比较的是地址值
    }

正确做法是将传入的Integer对象进行拆箱进行比较

复制代码
 /**
     * 正确比较包装对象做法是进行手动拆箱
     */
    @Test
    public void BoxedCompareTest2() {
        Comparator<Integer> naturalOrder = (i, j) -> {
            int compaer1 = Integer.valueOf(i);
            int compaer2 = Integer.valueOf(j);
            return (compaer1 < compaer2) ? -1 : (compaer1 == compaer2 ? 0 : 1);
        };
        int compare = naturalOrder.compare(new Integer(12), new Integer(12));
        System.out.println(compare);//返回1 因为integer new出来的对象比较的是地址值
    }

包装类使用时可能会出现的性能问题

使用包装类和基本类型运算会会导致频繁拆装箱,通过查看字节码,我们可以看到第一段 sum += i;实际上会进行INVOKESTATIC java/lang/Long.valueOf (J)Ljava/lang/Long;进行装箱操作。所以我们应该避免这种情况,在进行大量的数值计算时尽量使用基本类型

复制代码
/**
     * 频繁拆装箱会导致性能问题
     */
    @Test
    public void boxedSum() {
        long start = System.currentTimeMillis();
        Long sum = 0l;
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            //会导致频繁装箱
//            INVOKESTATIC java/lang/Long.valueOf (J)Ljava/lang/Long;
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);//4723

        long sum1 = 0l;
        start = System.currentTimeMillis();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            sum1 += i;
        }
        end = System.currentTimeMillis();
        System.out.println(end - start);//519
    }

空指针问题

由于自动拆装箱机制,下面这段代码会进行自动拆箱,unbelievable 会调用intValue转为整数再和12进行比较,而导致空指针异常:

复制代码
public class Unbelievable {
    static Integer unbelievable;

    public static void main(String[] args) {
//        包装类型会进行自动拆箱 调用valueOf
        // INVOKEVIRTUAL java/lang/Integer.intValue ()I
//  所以很可能导致空指针
        if (unbelievable == 12) {
            System.out.println("Unbelievable");
        }
    }

}

Java中对高精度数据的运算

Java中数据精度丢失的问题

可以看到进行+2操作时候报错了,因为byte+int类型时会自动转为高精度的类型,即int,而byte无法容纳int的数字,所以报错了。

那为何byte b=3可以通过呢?在进行赋值操作时,编译器会检查赋的值的取值范围是否在数据类型以内,题目中值的范围符合要求。

所以如果我们要实现加整型的解决方案就只能进行强转。

bash 复制代码
byte b=(byte)(b+2);

浮点数运算错误

如下代码所示,计算机使用二进制表示小数时可能会出现计算循环,由于精度截断很可能导致计算结果错误

复制代码
        /**
         * 小数精度丢失原因
         *  0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
         * 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
         * 0.2 * 2 = 0.4 -> 0
         * 0.4 * 2 = 0.8 -> 0
         * 0.8 * 2 = 1.6 -> 1
         * 0.6 * 2 = 1.2 -> 1
         * 0.2 * 2 = 0.4 -> 0(发生循环)
         * ...
         */
        float result1 = 3.0f - 2.9f;
        float result2 = 2.9f - 2.8f;
        System.out.println(result1);
        System.out.println(result2);
        System.out.println(result1 == result2);

如何解决浮点数精度丢失问题

对于高精度运算,JDK已经为我们提供了一个不错的计算工具BigDecimal

复制代码
        BigDecimal num1 = new BigDecimal("3.0");
        BigDecimal num2 = new BigDecimal("2.9");
        BigDecimal num3 = new BigDecimal("2.8");
        //减法
        BigDecimal result1 = num1.subtract(num2);
        BigDecimal result2 = num2.subtract(num3);
        System.out.println(result1);//0.1
        System.out.println(result2);//0.1
        System.out.println(result1.equals(result2));//true

超过long类型的整数如何标识

如下所示,使用常规类型计算超过long的值会造成计算错误,所以我们可以使用bigInteger解决问题

java 复制代码
  long maxVal = Long.MAX_VALUE;
        System.out.println(maxVal);

        /**
         * 加1后高位变1成为负数
         */
        long maxValAdd = maxVal + 1;
        System.out.println(maxValAdd);
        System.out.println(maxValAdd == Long.MIN_VALUE);

        //超过long类型的计算方式
        BigInteger bigInteger = new BigInteger("9223372036854775807");
        System.out.println(bigInteger.add(new BigInteger("1")));

高性能精度运算技巧

我们来看一个例子,代码如下所示,我们想看看一块钱可以把多少块糖果,注意糖果每次循环结果会涨0.1元。

首先我们使用常规double类型运算:

复制代码
        double funds = 1.00;
        int itemCount = 0;
        for (double price = 0.10; funds >= price; price += 0.10) {
            System.out.println("funds:" + funds + "     price:" + price);
            funds -= price;
            itemCount++;
            System.out.println("remain:" + funds + "   itemCount:" + itemCount);

输出结果,可以看到精度计算异常了

复制代码
funds:1.0     price:0.1
remain:0.9   itemCount:1
funds:0.9     price:0.2
remain:0.7   itemCount:2
funds:0.7     price:0.30000000000000004
remain:0.3999999999999999   itemCount:3

我们将计算方式修改为使用BigDecimal,虽然可以解决问题,但是性能差、操作不便:

复制代码
        final BigDecimal TEN_CENT = new BigDecimal("0.10");
        BigDecimal funds = new BigDecimal("1.00");
        int itemCount = 0;
        for (BigDecimal price = TEN_CENT;
             funds.compareTo(price) >= 0;
             price = price.add(new BigDecimal("0.10"))) {
            System.out.println("funds:" + funds + "     price:" + price);
            funds = funds.subtract(price);
            itemCount++;
            System.out.println("remain:" + funds + "   itemCount:" + itemCount);
   }

我们比较推荐结合业务,小数变为整数进行计算,代码如下所示,我们完全可以结合业务场景决定将这段代码转为整数完成计算,如下所示,笔者将所有数值都乘10,这样一来所有计算都是整数计算,从而避免BigDecimal 运算的开销:

复制代码
        int funds = 100;
        int itemCount = 0;
        for (double price = 10; funds >= price; price += 10) {
            System.out.println("funds:" + funds + "     price:" + price);
            funds -= price;
            itemCount++;
            System.out.println("remain:" + funds + "   itemCount:" + itemCount);

        }

一些常见的笔试题

请问下面两行代码是否会报错?

复制代码
double a=2.30f;
 float b=2.3d;

答案:

java 复制代码
第一行不会报错,double类型指向float类型,第二行报错,double类型赋值给float类型。

以下哪个不是Java的基本数据类型

String不会基本类型

  1. byte
  2. short
  3. String
  4. int

以下代码输出结果是?

复制代码
		  Integer a = -133;
        Integer b = -133;
        System.out.println(a == b);
        Integer c = 1;
        int d = 1;
        System.out.println(c == d);

答案:

java 复制代码
第一个输出为false Integer值的缓存分为为-128-127
第二个输出为 true Integer c = 1;会拆箱

在J2EE的Web应用中,编译后的class文件存放的目录为

复制代码
A. classes目录
B. images目录
C. jar目录
D. 任意位置

答案:

java 复制代码
A

http缺省的请求方式是

默认是GET请求。

以下代码输出的结果是

复制代码
		int a=2;
        float b=2.0f;
        System.out.println(a==b);

答案

java 复制代码
true

以下代码输出结果

复制代码
  System.out.println(Math.round(99.5)); 
 System.out.println(Math.ceil(99.5)); 

答案:

java 复制代码
第一个是四舍五入为100,第二个直接向上取整为100

随机生成一个100-999的随机数

要生成在min,max之间的随机整数,可使用Random类进行相关运算:

random.nextInt(max)表示生成0,max之间的随机数,然后对(max-min+1)取模。

以生成1000,10000随机数为例,

首先生成0-10000的随机数,然后对(10000-1000+1)取模得到0-1000之间的随机数,

然后加上min=1000,最后生成的是1000-10000的随机数

java 复制代码
Random random = new Random();
int s = random.nextInt(max)%(max-min+1) + min;

另外也可:

java 复制代码
random.nextInt(max - min) + min + 1

float a=3.3;是否会报错,为什么,float有几种声明方式?

错误,正确的写法如下:

java 复制代码
1.  float a=3.3f;
2. float b=0x03;
3. float c=3.3F;
4. float d=55;

指出以下代码的输出结果

复制代码
 String a = new String("ab");
        String b = new String("ab");
        String aa = "ab";
        String bb = "ab";
        if (aa == bb) {
            System.out.println("aa==bb");
        }
        if (a == b) {
            System.out.println("a==b");
        }
        if (a.equals(b)) {
            System.out.println("aEQb");
        }

        if (42 == 42.0) {
            System.out.println("true");
        }

输出结果

复制代码
aa==bb
aEQb
true

项目中用到那些Java8新特性?

  1. lambda
  2. streams
  3. 并行流
  4. optional
  5. Date/Time API

具体可以参考

JAVA8 十大新特性详解

参考文献

Java transient关键字:https://cloud.tencent.com/developer/article/1179627#:~:text=transien,么情况下会失效。

java中native的用法:https://www.cnblogs.com/b3051/p/7484501.html

Java基础常见面试题总结(上):https://javaguide.cn/java/basis/java-basic-questions-01.html#基本语法

Java系统性能优化实战:https://book.douban.com/subject/34879022/#:~:text=《Java系统性,能Java系统。

Effective Java中文版(第3版):https://book.douban.com/subject/30412517/

面渣逆袭(Java基础篇面试题八股文):https://tobebetterjavaer.com/sidebar/sanfene/javase.html#_7-java-有哪些数据类型

相关推荐
OxYGC19 分钟前
[AI工程] Spring AI 第十四篇:Agent 五种模式在 2.0 里怎么写
java·spring·ai·ai编程
code2cat19 分钟前
【随笔】从聊天到调用工具:理解MCP在AI应用中的位置
java·人工智能
2501_9339232528 分钟前
Spring Bean作用域揭秘:单例、原型、请求与会话的区别
java·后端·spring·java-ee
学编程就要猛1 小时前
基于Spring AI 的智能聊天机器人
java·spring ai·chat robot
修炼室1 小时前
Java开发工程师笔试经验贴【高频知识】
java
木井巳1 小时前
【记忆化搜索】最长递增子序列
java·算法·leetcode·深度优先·剪枝·推荐算法
杨杨杨大侠1 小时前
知识库已经有了,Java 程序员还要做什么?Spring AI RAG 实战
java·openai·ai编程
伊信1 小时前
Spring AI 基础学习与应用
java·后端
夜郎king1 小时前
基于 Java + Playwright 实现网站自动访问与数据采集(以腾讯云开发者社区为例)
java·playwright·网页数据获取
Anastasiozzzz1 小时前
重新定义 Agent 基建:Redis 在现代 AI 与智能体系统中的工程实践
java·人工智能·redis·ai