【JavaGuide面试总结】Java基础篇·中
- 1.重载和重写有什么区别?
- 2.什么是可变长参数?
- 3.为什么浮点数运算的时候会有精度丢失的风险?
- 4.如何解决浮点数运算的精度丢失问题?
- 5.超过 long 整型的数据应该如何表示?
- 6.基本类型和包装类型的区别?
- 7.包装类型的缓存机制了解么?
- 8.自动装箱与拆箱了解吗?原理是什么?
- 9.对象的相等和引用相等的区别
- 10.面向对象三大特征
1.重载和重写有什么区别?
重载
重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理
另外,重载是有规定的:
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。 如果编译器找不到匹配的参数, 就会产生编译时错误, 因为根本不存在匹配, 或者没有一个比其他的更好(这个过程被称为重载解析)。🍳
重写
重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。
- 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为
private/final/static
则子类就不能重写该方法,但是被static
修饰的方法能够被再次声明。 - 构造方法无法被重写
2.什么是可变长参数?
所谓可变长参数就是允许在调用方法时传入不定长度的参数,可变参数只能作为函数的最后一个参数
public class Main {
public static void main(String[] args) {
test("tom", "lili", "wenwen");
// tom
// lili
// wenwen
}
public static void test(String... kill) {
for (String s : kill) {
System.out.println(s);
}
}
}
Java 的可变参数编译后实际会被转换成一个数组🧇
那么,我们快来看一个有意思的问题,当可变参数遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?
答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
public class Main {
public static void main(String[] args) {
test("tom", "lili"); // 非可变参数的方法tom lili
}
public static void test(String... kill) {
for (String s : kill) {
System.out.println("可变参数的方法" + s);
}
}
public static void test(String a, String b) {
System.out.println("非可变参数的方法" + a + " " + b);
}
}
3.为什么浮点数运算的时候会有精度丢失的风险?
浮点数运算精度丢失代码演示:
public class Main {
public static void main(String[] args) {
float f1 = 1.2f - 1.1f;
float f2 = 0.9f - 0.8f;
System.out.println(f1); // 0.100000024
System.out.println(f2); // 0.099999964
System.out.println(f1 == f2); // false
}
}
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
小数部分转二进制字符串的方法:
把小数乘以2,假设结果是s,如果s大于1,该位二进制值为1,如果小于1,该位二进制值为0,直到不存在小数为止
例如,计算机无法准确的表示0.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(发生循环)
4.如何解决浮点数运算的精度丢失问题?
BigDecimal
可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal
来做的。
public class Main {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("1.2");
BigDecimal b = new BigDecimal("1.1");
BigDecimal c = new BigDecimal("0.9");
BigDecimal d = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = c.subtract(d);
System.out.println(x); // 0.1
System.out.println(y); // 0.1
System.out.println(x.compareTo(y)); // 0
}
}
5.超过 long 整型的数据应该如何表示?
BigInteger
内部使用 int[]
数组来存储任意大小的整形数据。
相对于常规整数类型的运算来说,BigInteger
运算的效率会相对较低。
public class Main {
public static void main(String[] args) {
BigInteger a = new BigInteger("100010001000100010001000100010001");
BigInteger b = new BigInteger("99999999999999999999");
System.out.println(a.add(b)); // 100010001000200010001000100010000
}
}
6.基本类型和包装类型的区别?
- 包装类型可用于泛型,而基本类型不可以。
- 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被
static
修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。 - 相比于对象类型, 基本数据类型占用的空间非常小。
被
static
声明的成员变量属于静态成员变量,静态变量存放在Java 内存区域的方法区。🍞
7.包装类型的缓存机制了解么?
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
Integer 缓存源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static {
// high value may be configured by property
int h = 127;
}
}
Boolean
缓存源码:
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
Character
缓存源码:
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
private static class CharacterCache {
private CharacterCache(){}
static final Character cache[] = new Character[127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Character((char)i);
}
}
两种浮点数类型的包装类
Float
,Double
并没有实现缓存机制。🥐
接下来我们来看一个示例更深刻的理解一下包装类型的缓存机制吧:
public class Main {
public static void main(String[] args) {
Integer a = 100; // 会发生自动装箱
Integer b = 100;
System.out.println(a == b); // true - 因为包装类型的缓存机制,a与b是同一个对象
Integer c = 521;
Integer d = 521;
System.out.println(c == d); // false - 超过了缓存的范围,因此c与d不是一个对象
}
}
因为缓存机制的影响,所以我们约定一个规矩:
所有整型包装类对象之间值的比较,全部使用 equals
方法比较
8.自动装箱与拆箱了解吗?原理是什么?
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
Integer i = 10; // 装箱
int n = i; // 拆箱
装箱其实就是调用了包装类的valueOf()
方法,拆箱其实就是调用了 xxxValue()
方法。
因此,
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
;
注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
9.对象的相等和引用相等的区别
- 对象的相等一般比较的是内存中存放的内容是否相等。
- 引用相等一般比较的是他们指向的内存地址是否相等。
10.面向对象三大特征
封装:
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
public class Student {
private int id; // id属性私有化
private String name; // name属性私有化
// 获取id的方法
public int getId() {
return id;
}
// 设置id的方法
public void setId(int id) {
this.id = id;
}
// 获取name的方法
public String getName() {
return name;
}
// 设置name的方法
public void setName(String name) {
this.name = name;
}
}
继承:
不同类型的对象,相互之间经常有一定数量的共同点。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
关于继承如下 3 点请记住:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
多态:
表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
多态的特点:
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
- 多态不能调用“只在子类存在但在父类不存在”的方法;
- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;