一、基本数据类型
Java 的基础数据类型,也被称为原始数据类型,是编程语言中最基本的数据类型,用于存储数字、字符和布尔值。在 Java 中,有 8 种基本数据类型,分为两类:数值类型和布尔类型。以下是 Java 的基础数据类型以及一些与它们相关的常见面试问题:
1. 数值类型:
- byte:8 位,范围为 -128 到 127。
- short:16 位,范围为 -32,768 到 32,767。
- int:32 位,范围为 -2,147,483,648 到 2,147,483,647。
- long:64 位,范围为 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。
- float:32 位,单精度浮点数,用于存储近似值,通常用于科学和工程计算。
- double:64 位,双精度浮点数,用于存储更精确的浮点数,通常用于财务和精密计算。
2.布尔类型
- boolean :用于表示布尔值,只有两个值,即
true
和false
。
二、自动装箱和拆箱
自动装箱(Autoboxing)和拆箱(Unboxing)是 Java 语言中的两个特性,用于在基本数据类型和其对应的包装类型之间进行转换。这些特性使得在基本数据类型和包装类型之间切换更加方便,而不需要显式的转换。
自动装箱(Autoboxing) : 自动装箱是指将基本数据类型的值自动封装到相应的包装类型中。例如,将一个整数值自动封装到Integer
对象中。这个特性使得代码更容易理解,同时也提高了代码的可读性。
例子:
自动拆箱(Unboxing) : 自动拆箱是指将包装类型对象自动拆开,取出其中的基本数据类型值。例如,从Integer
对象中自动拆箱出一个整数值。这个特性使得在需要使用基本数据类型时更加方便。
例子:
Java 中的自动装箱和拆箱是编译器级别的特性,这意味着编译器会自动插入相关的装箱和拆箱代码,以便你可以轻松地在基本数据类型和包装类型之间进行转换。除了自动装箱和拆箱,你也可以使用显式的方式进行转换,如使用valueOf
方法来装箱或使用.intValue()
等方法来拆箱。但自动装箱和拆箱特性在编写代码时更加便捷和可读。但需要注意,自动装箱和拆箱可能会带来一些性能开销,因此在性能敏感的代码中需要谨慎使用。
三、精度问题
浮点数精度问题是在计算机中使用浮点数表示实数时可能出现的精度损失或不精确性的情况。这是因为计算机内部使用有限的二进制位来表示实数,而实数通常是无限精度的。浮点数精度问题可能导致计算结果与预期的结果不完全一致,尤其在需要高精度计算的情况下会更为显著。
主要的浮点数精度问题包括以下几种情况:
1. 舍入误差:浮点数通常以二进制形式存储,这可能导致一些小数无法以精确二进制形式表示。当对这些浮点数执行算术运算时,可能会出现舍入误差。
2. 精度丢失:在浮点数运算中,如果一个浮点数具有更高的精度,但结果需要存储在较低精度的浮点数中,会发生精度丢失。例如,将一个双精度浮点数转换为单精度浮点数可能会导致精度丢失。
3. 比较问题:由于浮点数表示不精确,进行相等性比较可能会出现问题。在比较浮点数时,应该使用一个小的误差范围来检查它们是否足够接近。
为了避免浮点数精度问题,可以采取以下措施:
1. 使用适当的数据类型 :根据问题的需求,选择合适的浮点数精度。如果需要更高的精度,可以使用double
(双精度浮点数)而不是float
(单精度浮点数)。
2. 避免无限循环:避免使用浮点数进行无限循环,因为舍入误差可能会导致浮点数永远无法等于某个特定值。
3. 比较时使用容忍误差:在进行相等性比较时,使用一个小的容忍误差范围来检查浮点数是否接近目标值,而不是精确相等。
4. 考虑使用 BigDecimal :对于需要高精度的金融计算等情况,可以考虑使用BigDecimal
类,它提供了精确的十进制表示,避免了浮点数精度问题。
四、基本数据类型和引用数据类型之间的区别
1. 存储方式:
- 基本数据类型:基本数据类型的变量直接存储实际的数据值。它们在栈内存中分配空间。
- 引用数据类型:引用数据类型的变量存储的是对象的引用或内存地址,而不是实际的数据值。对象本身通常存储在堆内存中,而引用存储在栈内存中。
2. 内存分配:
- 基本数据类型的内存分配是固定大小的,与数据类型的大小相关,不受对象大小或内容的影响。
- 引用数据类型的内存分配取决于对象的大小,对象可以包含不同数量的数据成员。
3. 默认值:
- 基本数据类型的变量在声明时会自动初始化为其默认值。例如,
int
类型的变量默认值为0。 - 引用数据类型的变量在声明时会自动初始化为
null
,表示它不引用任何对象。
4. 操作:
- 基本数据类型的变量存储的是实际数据值,可以进行各种算术和比较操作。
- 引用数据类型的变量存储的是对象引用,需要通过该引用来访问对象的成员和方法。
5. 传递方式:
- 基本数据类型在方法间传递时是按值传递,即传递的是实际值的副本。
- 引用数据类型在方法间传递时也是按值传递,但传递的是对象引用的副本,所以对于相同对象的多个引用仍指向同一个对象。
6. 封装性:
- 基本数据类型没有封装性,不具备成员和方法。
- 引用数据类型可以有成员和方法,因为它们通常是对象的实例。
基本数据类型包括整数、浮点数、字符和布尔值等,如int
、double
、char
和boolean
。引用数据类型包括类、接口、数组等,如自定义类、字符串、集合类和数组对象。
五、Java 中使用 BigDecimal 而不是 double 来处理金融数据
1. 浮点数精度问题 :double
是浮点数,采用二进制表示,因此不能准确表示所有的十进制小数。这可能导致在金融计算中出现舍入误差,尤其是在累积多次计算时。这种误差可能会导致金融计算的不准确性,特别是在涉及大金额或复杂计算的情况下。
2. 精度控制 :BigDecimal
允许你显式控制小数点后的位数。这对于需要特定精度的金融计算非常有用,你可以指定所需的精度,而不受底层表示的限制。
3. 避免舍入问题 :使用 BigDecimal
可以避免舍入误差。BigDecimal
提供了精确的数学运算,可以确保计算结果与数学期望一致。
4. 安全性 :在金融领域,精度和准确性是至关重要的,特别是在涉及货币、税收、利率等关键数据时。使用 BigDecimal
有助于确保金融计算的准确性和可靠性,降低出错的可能性。
5. 可读性 :使用 BigDecimal
使代码更容易理解,因为它表明了你正在处理精确的金融数据,而不是浮点数的近似值。
使用BigDecimal时需要注意以下几点:
- 避免使用BigDecimal(double val)构造方法,应该使用BigDecimal(String val)构造方法。
- 使用setScale方法设置精度,并将返回值重新赋值给原对象。
- 使用compareTo方法比较大小,而不是使用==或!=。
- 使用stripTrailingZeros方法去掉末尾的0,并将返回值重新赋值给原对象。
- 避免使用BigDecimal的doubleValue方法,使用toBigInteger方法获取一个BigInteger类型的值,再使用doubleValue方法进行转换。
- 避免使用BigDecimal的equals方法,使用compareTo方法进行比较。
六、如何将字符串转换为数值类型
1. 使用包装类的 parseXxx() 方法:
- 对于整数类型,可以使用
Integer.parseInt()
方法。 - 对于浮点数类型,可以使用
Double.parseDouble()
方法。
示例:
注意:这些方法对于合法的数字字符串有效,如果字符串无法解析为有效的数字,将引发NumberFormatException
异常。因此,在使用这些方法时,最好使用异常处理机制来捕获潜在的异常。
2. 使用包装类的 valueOf() 方法:
- 你可以使用
Integer.valueOf()
方法将字符串转换为Integer
对象,然后使用.intValue()
方法获取int
值。 - 对于浮点数,可以使用
Double.valueOf()
和.doubleValue()
。
示例:
这种方法与前一种方法类似,但它不会引发异常,而是返回null
,如果字符串无法解析为有效数字。
3. 使用Scanner 类:
- 使用
java.util.Scanner
类可以从字符串中解析各种数据类型,包括整数和浮点数。
示例:
七、String类面试
1. String 为什么不可变
安全性:不可变的字符串更加安全,特别是在多线程环境下。因为字符串是不可变的,多个线程可以同时访问字符串,而不必担心其他线程修改它。这减少了并发问题的可能性。
缓存和性能:由于字符串是不可变的,它们可以被缓存,以便多个字符串变量引用相同的字符串常量。这可以节省内存,提高性能,因为字符串常量可以被多次重用。
哈希值和安全性:字符串的哈希值通常在字符串创建时计算,并且由于字符串是不可变的,哈希值可以缓存。这提高了哈希表等数据结构的性能。
字符串连接优化:不可变字符串允许编译器执行字符串连接的优化。在循环中连接字符串时,编译器可以在编译时优化字符串连接操作,减少了不必要的字符串对象的创建。
传递不变性:如果字符串是可变的,传递字符串参数可能导致不期望的副作用。因为字符串是不可变的,你可以放心地将字符串传递给方法,而不必担心它被修改。
2. String, StringBuffer 和 StringBuilder区别
在Java中,String
、StringBuffer
和StringBuilder
都用于处理字符串,但它们之间有一些重要的区别,主要涉及到字符串的不可变性和性能方面:
不可变性:
String
是不可变的,一旦创建了String
对象,它的内容不能被修改。如果对String
执行字符串操作,实际上会创建一个新的String
对象。StringBuffer
和StringBuilder
是可变的,它们允许在原始对象上执行字符串操作,而不创建新对象。
线程安全性:
String
是线程安全的,因为它是不可变的。多个线程可以同时访问一个String
对象,而不会引发并发问题。StringBuffer
是线程安全的,它提供了同步方法,可以安全地在多线程环境中进行操作。StringBuilder
不是线程安全的,它没有同步方法,适用于单线程环境。
性能:
- 由于
String
是不可变的,对String
的任何修改都需要创建新的String
对象,这可能导致性能开销,特别是在大量字符串操作时。 StringBuffer
和StringBuilder
是可变的,因此在执行大量字符串操作时性能更好。StringBuffer
是线程安全的,而StringBuilder
在单线程环境下性能更高,因为它不涉及同步。
适用场景:
- 使用
String
当你需要一个不可变的字符串,特别是在涉及多线程或缓存字符串常量的情况下。 - 使用
StringBuffer
当你需要一个可变的字符串,但要保证线程安全。 - 使用
StringBuilder
当你需要一个可变的字符串,且在单线程环境下,以获得更好的性能。
总之,选择哪种字符串类取决于你的需求。如果你需要不可变性和线程安全性,选择String
或StringBuffer
。如果你需要可变性,并且在单线程环境中,并且关注性能,选择StringBuilder
。根据特定情况的要求,可以灵活选择适当的类。
3、String 类的常用方法
获取字符串长度:
int length()
:返回字符串的长度,即字符的数量。
字符串连接:
String concat(String str)
:将指定字符串连接到原字符串的末尾。
字符串截取:
String substring(int beginIndex)
:返回从指定索引开始到字符串末尾的子字符串。String substring(int beginIndex, int endIndex)
:返回从开始索引到结束索引之间的子字符串。
字符串比较:
boolean equals(Object obj)
:比较字符串是否与指定对象相等。boolean equalsIgnoreCase(String anotherString)
:忽略大小写比较字符串是否相等。int compareTo(String anotherString)
:按字典顺序比较字符串。
查找子字符串:
int indexOf(String str)
:查找指定子字符串第一次出现的位置。int lastIndexOf(String str)
:查找指定子字符串最后一次出现的位置。
替换字符串:
String replace(char oldChar, char newChar)
:用新字符替换字符串中的旧字符。String replace(CharSequence target, CharSequence replacement)
:用新的字符序列替换字符串中的目标字符序列。
移除空格:
String trim()
:移除字符串两端的空格。
大小写转换:
String toLowerCase()
:将字符串转换为小写。String toUpperCase()
:将字符串转换为大写。
判断字符串内容:
boolean isEmpty()
:检查字符串是否为空。boolean startsWith(String prefix)
:检查字符串是否以指定前缀开头。boolean endsWith(String suffix)
:检查字符串是否以指定后缀结尾。
判断字符串包含:
boolean contains(CharSequence sequence)
:检查字符串是否包含指定的字符序列。
分割字符串:
String[] split(String regex)
:根据正则表达式将字符串拆分为字符串数组。
格式化字符串:
static String format(String format, Object... args)
:使用指定的格式字符串和参数创建格式化的字符串。
转换为字符数组:
char[] toCharArray()
:将字符串转换为字符数组
获取指定位置的字符:
char charAt(int index)`:返回指定索引位置的字符。
4、new String("dabin")会创建几个对象
字符串池中的对象:首先,会在字符串池(String Pool)中查找是否存在字符串值为 "dabin" 的对象。如果字符串池中不存在该字符串,则会在字符串池中创建一个新的字符串对象 "dabin",然后将该对象的引用返回给变量。
堆内存中的对象 :然后,使用new
关键字会在堆内存中创建一个新的字符串对象,这个对象也包含字符串值 "dabin"。这个新创建的字符串对象不会被添加到字符串池中,它是一个独立的对象。
所以,new String("dabin")
表达式实际上创建了两个字符串对象,一个在字符串池中,另一个在堆内存中。通常情况下,如果不需要特意在堆内存中创建新的字符串对象,可以直接使用字符串字面量(如"dabin"
)来避免不必要的对象创建。
5、字符串常量池
字符串常量池(String Pool)是Java中的一个特殊内存区域,用于存储字符串字面量,以避免重复创建相同值的字符串对象。以下是关于字符串常量池的一些重要信息:
字符串字面量 :字符串字面量是在代码中直接写的字符串值,如 "hello"
。这些字符串字面量在编译时被添加到字符串常量池中。
字符串池存储:字符串常量池通常位于方法区(在Java 8之前)或元空间(在Java 8及以后)中。每个类加载时,字符串常量池会初始化并存储在内存中。
不可变性:字符串池中的字符串对象是不可变的,它们的内容在创建后不能被修改。这使得字符串常量池更加安全,因为多个引用可以共享相同的字符串对象。
字符串常量共享:字符串常量池确保相同的字符串字面量只在内存中存在一次。当多个部分的代码使用相同的字符串字面量时,它们实际上引用相同的对象。
字符串池的使用 :在Java中,可以使用字符串字面量直接创建字符串对象,如 String str = "hello";
。如果存在相同的字符串字面量,它们将共享相同的对象。
字符串池的影响 :由于字符串常量池的存在,当对字符串进行比较时,可以使用equals()
方法来比较字符串的内容,而不需要使用==
来比较对象引用。
使用intern()
方法 :String
类提供了intern()
方法,可以将字符串对象添加到字符串常量池中。这可以用于显式地将字符串对象放入池中,以便共享。