Java面试题

1、JavaSE

JDK、JRE、JVM有什么区别?

JDK(Java Development Kit)是Java开发工具包,它包含了编译器、调试器、JavaDoc等工具和类库,用于开发和编译Java应用程序。

JRE(Java Runtime Environment)是Java运行环境,它包含了Java虚拟机(JVM)和Java库等组件,用于运行Java应用程序。

JVM(Java Virtual Machine)是Java虚拟机,它是Java程序的运行环境。JVM可以在不同的平台上运行Java代码,它会将Java代码解释成平台相关的机器指令,以保证Java代码在不同平台上的可移植性。

因此,JDK是用于开发和编译Java应用程序的工具包,JRE是用于运行Java应用程序的运行环境,而JVM则是Java程序的运行环境。

延伸问题:Java为什么可以跨平台?可以聊一聊操作系统的API吗?

常用数字类型的区别?

Java中常用的数字类型有整数类型和浮点数类型。整数类型包括byte、short、int和long,浮点数类型包括float和double。它们的区别如下:

  1. byte类型:占用一个字节,取值范围是-128到127。
  2. short类型:占用两个字节,取值范围是-32768到32767。
  3. int类型:占用四个字节,取值范围是-2147483648到2147483647。
  4. long类型:占用八个字节,取值范围是-9223372036854775808到9223372036854775807。
  5. float类型:占用四个字节,有效位数为6-7位,取值范围为±1.4E-45到±3.4028235E+38。
  6. double类型:占用八个字节,有效位数为15位,取值范围为±4.9E-324到±1.7976931348623157E+308。

总的来说,整数类型适用于需要精确计算的场合,浮点数类型适用于对精度要求不高,但需要处理大量数据的场合。同时,double类型的精度比float类型更高,但也占用更多的内存空间。
在这里插入图片描述

Float在JVM的表达方式及使用陷阱?
在JVM中,float类型的数据被表示为32位的二进制数,由1个符号位、8个指数位和23个尾数位组成。其中,符号位用于表示正负数,指数位用于表示数值的大小范围,尾数位用于表示数值的精度。在Java程序中,可以使用float关键字定义一个float类型的变量。

float类型的数据在计算时,可能会遇到精度丢失的问题。这是因为float类型的数据只能表示有限的小数,无法精确表示一些浮点数,如0.1、0.2等。在进行浮点数的计算时,可能会出现舍入误差,导致计算结果与预期的结果不一致。

另外,要注意在比较两个float类型的变量时,应该使用浮点数的比较方法,而不是使用等于或不等于操作符。这是因为在计算机内部,浮点数的值是以二进制形式表示的,因此在比较时可能会出现精度误差,导致比较结果与预期的结果不一致。为了避免这种问题,应该使用Java API中提供的Float.compare()方法或者使用一个差值来进行比较,例如:

float a = 0.1f + 0.2f;
float b = 0.3f;
if (Math.abs(a - b) < 0.0001f) {
    System.out.println("a equals b");
} else {
    System.out.println("a not equals b");
}

在这个例子中,我们使用了一个差值0.0001f来判断a和b是否相等,从而避免了精度误差导致的问题。

控制台输出 “a equals b”。

这是因为浮点数在计算机内部是以二进制的形式存储的,并且无法准确地表示某些十进制数。例如,0.1在二进制下是无限循环的,因此在进行浮点数计算时可能会出现精度误差。在这个例子中,0.1f和0.2f在计算时可能会导致精度误差,但是由于0.3f在二进制下可以精确表示,所以比较它们的差值和一个小的阈值就可以得到正确的结果。

随机数的使用(生成30-110内的随机数)?

在Java中,可以使用Java自带的Random类来生成随机数。为了生成30到110之间的随机数,我们可以使用Random.nextInt()方法生成0到80之间的随机数,然后再将结果加上30即可。具体实现代码如下:

import java.util.Random;

public class RandomExample {
    public static void main(String[] args) {
        Random random = new Random();
        int randomNumber = random.nextInt(81) + 30;
        System.out.println("随机数为:" + randomNumber);
    }
}

在代码中,我们首先创建了一个Random对象,然后使用nextInt()方法生成0到80之间的随机数,再将结果加上30,得到30到110之间的随机数,并输出结果。

除了使用Java自带的Random类生成随机数之外,还可以使用Math.random()方法和ThreadLocalRandom类来生成随机数。具体方法如下:

  1. Math.random()方法

Math.random()方法可以生成0到1之间的随机数,具体实现代码如下:

int min = 30;
int max = 110;
int randomNumber = (int) (Math.random() * (max - min + 1)) + min;
System.out.println("随机数为:" + randomNumber);

在代码中,我们首先指定了随机数的范围(30到110之间),然后使用Math.random()方法生成0到1之间的随机数,将其乘以范围差值(即max - min + 1),再加上最小值,得到30到110之间的随机数。

​ 2.ThreadLocalRandom类

ThreadLocalRandom类是Java 7中新增的一个线程安全的随机数生成器,具体实现代码如下:

import java.util.concurrent.ThreadLocalRandom;

public class RandomExample {
    public static void main(String[] args) {
        int min = 30;
        int max = 110;
        int randomNumber = ThreadLocalRandom.current().nextInt(min, max + 1);
        System.out.println("随机数为:" + randomNumber);
    }
}

在代码中,我们使用ThreadLocalRandom.current()方法获取当前线程的ThreadLocalRandom实例,然后使用nextInt()方法生成30到110之间的随机数。值得注意的是,nextInt()方法的参数是左闭右开区间,因此需要将最大值加1。

找出1-1000内的质数?

要找出1到1000之间的质数,可以使用素数筛法,它是一种常用的高效算法,可以在较短的时间内找出指定范围内的所有质数。具体实现代码如下:

public class PrimeNumbersExample {
    public static void main(String[] args) {
        int n = 1000;
        boolean[] isPrime = new boolean[n + 1];

        // 初始化数组,默认都是质数
        for (int i = 2; i <= n; i++) {
            isPrime[i] = true;
        }

        // 筛选质数
        for (int i = 2; i <= Math.sqrt(n); i++) {
            if (isPrime[i]) {
                // 将i的倍数标记为合数
                for (int j = i * i; j <= n; j += i) {
                    isPrime[j] = false;
                }
            }
        }

        // 输出所有质数
        System.out.println("1到1000之间的质数有:");
        for (int i = 2; i <= n; i++) {
            if (isPrime[i]) {
                System.out.print(i + " ");
            }
        }
    }
}

在代码中,我们首先创建了一个布尔类型的数组isPrime,用于记录每个数是否是质数。将数组中所有元素初始化为true,然后从2开始遍历数组,如果某个数i是质数,那么将其所有的倍数标记为合数。最后遍历整个数组,输出所有的质数。

面向对象的三大特性是什么?

封装是指将数据和行为组合到一个单元中,并且对外只暴露出一些公共接口,以隐藏具体实现细节。这样做可以保护数据的完整性和安全性,同时也提高了代码的复用性和可维护性。

继承是指子类继承父类的属性和方法,以便可以重用父类的代码,同时还可以在此基础上进行扩展和特化。继承可以使得代码更加简洁,易于理解和维护。

多态是指同一个方法在不同的对象上有不同的表现形式。多态可以增强代码的灵活性和可扩展性,以便于程序员处理各种复杂的情况。

总的来说,这三个概念是互相联系的,它们可以协同工作以实现代码的可维护性、可扩展性和可重用性,这些特点是面向对象编程的主要优点。

当我们在现实生活中购买汽车时,汽车的制造商会封装引擎、制动系统和其他汽车部件的细节,只对用户暴露出一些公共接口,例如方向盘、刹车踏板和加速踏板。这样做的好处是用户只需要了解和操作这些公共接口就可以驾驶汽车了,而不用了解汽车的具体制造细节。

继承也可以在现实生活中找到类似的例子。例如,当我们购买一台电视时,电视的制造商可以利用现有的技术、设计和组件,生产新的型号。这些新型号可以继承已有的型号的某些特征和性能,例如屏幕分辨率、音频输出和电视尺寸等。这样做可以节省制造成本,并且使得消费者更容易理解和购买。

最后,多态可以类比于现实生活中不同的角色扮演。例如,演员可以扮演不同的角色,例如警察、医生、教师等。尽管他们是同一个人,但是在不同的角色下,他们的表现会有所不同。这就是多态的体现。

这些例子只是面向对象编程中封装、继承和多态的一些实际应用,它们可以帮助我们更好地理解这些概念,并且在实际编程中更加合理地运用它们。

静态与实例变量的区别?

静态变量和实例变量都是类的成员变量,但它们有一些重要的区别。

首先,静态变量属于类,而不属于类的任何一个实例对象。换句话说,无论创建多少个该类的实例对象,静态变量都只有一份拷贝,被所有实例对象所共享。而实例变量则是属于某个具体的实例对象的,每个实例对象都有一份自己的拷贝。

其次,静态变量在类被加载时就被初始化,而实例变量则是在创建实例对象时才被初始化。静态变量可以在不创建实例对象的情况下进行访问,而实例变量则必须通过实例对象进行访问。

最后,静态变量的生命周期与类相同,当类被卸载时才会销毁,而实例变量则是在实例对象被销毁时才会被销毁。

在使用时,需要根据实际情况选择使用静态变量或实例变量。例如,如果某个变量需要在多个实例对象之间共享或者需要在类加载时进行初始化,那么可以使用静态变量。而如果某个变量需要在每个实例对象中都有自己的拷贝,那么就需要使用实例变量。

当我们看到一些公共设施上的标识牌时,可以把这些标识牌看作是某个类的实例对象,而标识牌上的一些共同信息(例如颜色、图案等)可以看作是该类的静态变量,而标识牌上的一些个性化信息(例如名称、地址等)则可以看作是该类的实例变量。无论我们看到多少个标识牌,它们上面的共同信息都是一样的,这就是静态变量的特点;而标识牌上的个性化信息则因为不同的标识牌而不同,这就是实例变量的特点。

类的执行顺序?

在Java中,类的执行顺序包括以下步骤:

  1. 加载(loading):加载类的二进制数据到JVM中,生成对应的Class对象。
  2. 验证(verification):验证加载的类是否符合Java规范,包括语法、语义和安全性等方面。
  3. 准备(preparation):为类的静态变量分配内存空间,并设置默认初始值。
  4. 解析(resolution):将符号引用转换为直接引用。
  5. 初始化(initialization):执行类的静态代码块,并初始化静态变量。
  6. 使用(usage):创建对象实例,并使用类的方法。
  7. 卸载(unloading):当类不再被使用时,从JVM中卸载类的二进制数据。

需要注意的是,类的初始化是在使用时才进行的,也就是说在调用类的静态方法或者实例化对象时才会执行初始化操作。此外,如果一个类被其他类所依赖,那么它也会被加载和初始化。

Java的异常体系?
Java的异常体系主要由Throwable、Error、Exception三个类组成,其中Throwable是异常体系的根类,Error和Exception是Throwable的两个子类。

Error表示系统级错误或者资源耗尽等无法恢复的错误,如OutOfMemoryError、StackOverflowError等。一般情况下,程序无法处理Error,只能让程序中止。

Exception则表示程序运行时出现的错误,可以被程序处理并恢复正常运行,如NullPointerException、ArrayIndexOutOfBoundsException等。Exception又分为两类:

  • Checked Exception:在方法声明中必须声明并处理,如IOException、SQLException等。
  • Unchecked Exception:不需要在方法声明中声明,也可以不进行处理,如RuntimeException及其子类,如NullPointerException、ArrayIndexOutOfBoundsException等。

此外,Java还提供了一种特殊的异常:RuntimeException的子类Error,在使用时不需要进行捕获处理。通常情况下,程序应该处理Checked Exception,而对于Unchecked Exception则应该通过代码设计避免其出现。

String与字符串常量池?
在Java中,String是一种特殊的对象类型,用于表示文本字符串。String对象在创建后通常会被放入字符串常量池中,以便于复用和提高程序性能。

字符串常量池是一块固定大小的内存区域,用于存储字符串常量(即用双引号括起来的字符串字面量)以及通过String类的intern()方法将字符串对象放入池中。当程序需要创建一个新的字符串对象时,JVM会首先检查字符串常量池中是否已经存在一个等于该字符串的对象,如果存在,则直接返回该对象的引用;否则,创建一个新的对象并放入字符串常量池中,然后返回该对象的引用。

使用字符串常量池可以有效地减少内存使用,因为对于相同的字符串,只需要在常量池中存储一份即可。此外,由于String对象是不可变的,所以可以确保字符串常量池中的对象不会被修改。

需要注意的是,使用“+”符号拼接字符串时,会创建新的String对象,并不会直接将结果放入字符串常量池中。如果需要将拼接后的字符串放入常量池中,可以使用intern()方法。同时,由于字符串常量池是位于堆中的一部分,因此需要考虑内存泄漏的问题,即当不再需要某个字符串时,需要将其从常量池中移除。

String、StringBuilder与StringBuffer的区别?
在Java中,String、StringBuilder和StringBuffer都用于表示字符串,但它们之间有以下几个主要区别:

不可变性:String是不可变的,也就是说一旦一个String对象被创建出来,就不能再改变它的内容。而StringBuilder和StringBuffer是可变的,可以随时添加、删除或修改它们的内容。

线程安全性:String是线程安全的,因为它的值不可变。而StringBuilder和StringBuffer是可变的,因此它们是非线程安全的和线程安全的,分别对应StringBuilder和StringBuffer。

性能:由于String是不可变的,每次对String进行修改都会创建一个新的String对象,所以在需要频繁修改字符串时,使用StringBuilder和StringBuffer的性能会更好。StringBuffer在多线程环境下保证线程安全,但是由于对方法加锁的开销,相对于StringBuilder,在单线程环境下性能稍差一些。

综上所述,当需要频繁修改字符串并且不需要考虑线程安全的情况下,应该使用StringBuilder。当需要频繁修改字符串并且需要考虑线程安全的情况下,应该使用StringBuffer。当不需要修改字符串时,应该使用String。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

满天星...轻尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值