Java八股(基本概念和数据类型)

Java的特点

平台无关:由于JVM的存在,Java可以做到“一次编译,到处运行”的特点。这主要是因为,Java实际运行的是字节码文件(.class),而字节码文件是运行在Java虚拟机上的,所以只要安装了JVM就能到处运行。

面向对象:“一切皆对象”,Java是一门严格面向对象的编程语言,面向对象编程的特征能够让Java的代码具有:高可维护性和可复用性。Java通过“类、对象、多态、继承、抽象、封装”等一系列的概念完成了“面向对象”这一特性的实现。

内存管理:Java的JVM能够自动完成垃圾的自动回收,这样的特性能够自动回收不再使用的对象,降低内存泄露的风险。

Java的优势和劣势

Java的优势:

jar包生态丰富

内存自动回收机制

跨平台

支持多线程

稳定性高,方便向后兼容(jdk8 的方法,在 jdk11 升级了后任然能写老方法)

劣势:

性能差(开销大),比他性能好的有C++和Rust。

启动慢(微服务架构下),不如Go的启动速度快。

语法繁琐:相对于python这种语言,Java的语法显得繁琐很多,不过Lambda表达式的加入改善了一些这个缺点。代价是代码的可读性变差

开发效率低下

JVM本身也占内存

Java的数据类型

基本数据类型

  • byte
  • short
  • int
  • long
  • double
  • float
  • char
  • boolean

Java八种基本类型的字节数:1字节(byte、boolean)、 2字节(short、char)、4字节(int、float)、8字节(long、double)
浮点数的默认类型为double(如果需要声明一个常量为float型,则必须要在末尾加上f或F)
整数的默认类型为int(声明Long型在末尾加上l或者L)
八种基本数据类型的包装类:除了char的是Character、int类型的是Integer,其他都是首字母大写即可

Double类型的弊端

double执行的是二进制浮点运算,不能精确的表示一个小数,只能够表示能够用1/(2^n)的和的任意组合。

BigDecimal 这个类由此应运而生,精确计算 , 一般牵扯到金钱的计算 , 都使用BigDecimal。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.math.BigDecimal;

public class BigDecimalExample {
public static void main(String[] args) {
BigDecimal num1 = new BigDecimal("0.1");
BigDecimal num2 = new BigDecimal("0.2");

BigDecimal sum = num1.add(num2);
BigDecimal product = num1.multiply(num2);

System.out.println("Sum: " + sum);
System.out.println("Product: " + product);
}
}

//输出
Sum: 0.3
Product: 0.02

在创建BigDecimal对象时,应该使用字符串作为参数,而不是直接使用浮点数值,以避免浮点数精度丢失。

基本类型的强转

强制类型转换,可能会有数据丢失或溢出,进行转换之前,建议先检查long类型的值是否在int类型的值域范围内。

  • 自动类型转换(隐式转换):当目标类型的范围大于源类型时,Java会自动将源类型转换为目标类型,不需要显式的类型转换。例如,将int转换为long、将float转换为double等。
  • 强制类型转换(显式转换):当目标类型的范围小于源类型时,需要使用强制类型转换将源类型转换为目标类型。这可能导致数据丢失或溢出。例如,将long转换为int、将double转换为int等。语法为:目标类型 变量名 = (目标类型) 源类型。
  • 字符串转换:Java提供了将字符串表示的数据转换为其他类型数据的方法。例如,将字符串转换为整型int,可以使用Integer.parseInt()方法;将字符串转换为浮点型double,可以使用Double.parseDouble()方法等。
  • 数值之间的转换:Java提供了一些数值类型之间的转换方法,如将整型转换为字符型、将字符型转换为整型等。这些转换方式可以通过类型的包装类来实现,例如Character类、Integer类等提供了相应的转换方法。

对象类型的强转

在对对象类型进行强转的时候,请注意,被强转的对象得是目标子类的实例,否则会抛出运行时异常:ClassCastException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Animal animal = new Animal();
Dog dog = (Dog) animal; // 运行时抛出ClassCastException

//instanceof 关键字:可以用于检查实例是否是某类的实现。

//能进行强转的情况
Animal animal = new Dog();
Dog dog = (Dog) animal;

//或者增强健壮性:
Animal animal = new Dog();
if(animal instanceof Dog){
Dog dog = (Dog) animal;
}

包装数据类型

  • String(严格来说不算数据类型)
  • Integer
  • Character
  • Double
  • Byte
  • Long

拆箱和装箱

拆箱和装箱实际上就是指的基本数据类型和对应的包装类之间进行转换的过程。

1
2
Integer i = 10;  //装箱
int n = i; //拆箱

自动装箱

自动装箱主要发生在两种情况,一种是赋值时,另一种是在方法调用时。

赋值时的自动装箱

“自动装箱”这个“自动”的意思,就是在创建Integer 这个类并进行实例的初始化的时候,不需要调用valueof方法进行赋值了,Java5之后,这一过程在JVM中自动完成。

1
2
3
4
5
6
7
//手动装箱
Integer integer= Integer.valueOf(3); //Integer是静态类,可以用类名直接调用方法,valueOf会返回一个Integer的对象。
Int i = integer.intValue()

//after java5
Integer integer= 3; //自动装箱,直接写数字并赋值即可
int i = integer; //自动拆箱

方法调用时的自动装箱

当我们在方法调用时,我们可以传入原始数据值或者对象,同样编译器会帮我们进行转换。

例如某方法要求传入Integer类型的数据,可以用int类型的数字直接写入,编译器会帮助我们完成自动装箱。

1
2
3
4
5
6
7
8
public static Integer show(Integer iParam){
System.out.println("autoboxing example - method invocation i: " + iParam);
return iParam;
}

//show方法要求传入Integer类型的参数,但是传入了3,也能正常编译。是因为完成了自动装箱。
show(3); //自动装箱
int result = show(3); //又被自动拆箱了

自动装箱的弊端

在一个循环中进行自动装箱操作的时候,会产生大量的对象,严重影响程序性能。

1
Integer sum = 0; for(int i=1000; i<5000; i++){   sum+=i; } 

“不是自动装箱的弊端,而是引用类型本身不适合参与循环运算”

——这句话从技术本质上是对的

只要你选了 Integer 作为累加器,就注定会频繁产生新对象,跟是不是“自动”装箱关系不大。

用类名直接调用方法的情况

只有“静态方法(static)”才能用“类名.方法名()”来直接调用。

其他情况,必须先 new 对象,再 对象名.方法()

原因:
static 成员是“属于类本身而不是属于某个实例”的。JVM 在类加载时就已经把静态方法放在方法区,并且静态方法不依赖具体对象的实例字段,也没有 this。因此调用时不需要创建对象。

Java为什么要Integer

Integer对应是int类型的包装类,对象封装有很多好处,可以把属性也就是数据跟处理这些数据的方法结合在一起,比如Integer就有parseInt()等方法来专门处理int型相关的数据。

Java中绝大部分方法或类都是用来处理类类型对象的,如ArrayList集合类就只能以类作为他的存储对象,而这时如果想把一个int型的数据存入list是不可能的,必须把它包装成类,也就是Integer才能被List所接受。所以Integer的存在是很必要的。

在Java中,泛型只能使用引用类型,而不能使用基本类型。因此,如果要在泛型中使用int类型,必须使用Integer包装类。例如,假设我们有一个列表,我们想要将其元素排序,并将排序结果存储在一个新的列表中。如果我们使用基本数据类型int,无法直接使用Collections.sort()方法。但是,如果我们使用Integer包装类,我们就可以轻松地使用Collections.sort()方法。

利用包装类完成引用类型和基本数据类型的互转

在Java中,基本类型和引用类型不能直接进行转换,必须使用包装类来实现。例如,将一个int类型的值转换为String类型,必须首先将其转换为Integer类型,然后再转换为String类型。

1
2
3
4
int i = 10;
Integer integer = new Integer(i);
String str = integer.toString();
System.out.println(str);

包装类在集合中的应用

Java集合中只能存储对象,而不能存储基本数据类型。因此,如果要将int类型的数据存储在集合中,必须使用Integer包装类。

而集合类中有大量已经写好的方法。

包装类型的优点

使用int来存储一个整数时,不需要任何额外的内存分配,而使用Integer时,必须为对象分配内存。在性能方面,基本数据类型的操作通常比相应的引用类型快。

包装类型的优势不在“算得更快”,而在“能当对象用”——能为 null、能进集合、能做泛型参数、有方法、有常量、能参与反射和多态。

包装类可以是 null

基本类型做不到这一点:

1
2
int a = 0;      // 0 是一个具体的值
Integer b = null; // null = 真的“缺失/未知”

在很多业务场景里,“0” 和 “没有值”完全不是一回事,例如:

  • 用户年龄:0 岁 vs 未填写
  • 商品库存:0 件 vs 未初始化/未同步

可以用在集合和泛型里

Java 的泛型和集合不支持基本类型,只能用引用类型:

1
2
3
List<int> list;       // ❌ 不合法
List<Integer> list; // ✅ 正确
Map<Long, Integer> map; // ✅ 常见写法

原因很简单:集合里存的是“对象引用”,不是裸的 4 字节/8 字节值。

有方法、有常量,是“有行为的类型”

基本类型只有值,没有行为;

包装类型是类,有方法、有常量:

1
2
3
4
5
6
7
8
9
10
11
int x = 123;
String s1 = String.valueOf(x); // 借助工具方法

Integer y = 123;
String s2 = y.toString(); // 直接对象方法

Integer.parseInt("123"); // 静态解析
Integer.compare(a, b);
Integer.MAX_VALUE;
Long.MIN_VALUE;

包装类型 → 可以:

  • 提供工具方法(解析、比较、转换)
  • 提供边界常量MAX_VALUE / MIN_VALUE
  • 参与很多 API(方法参数是 Integer / Long 而不是 int

这就是“面向对象”的好处:值“长出了行为”

能参与多态、反射等

包装类型是对象,可以:

  • 当作 Object 传来传去:

    1
    2
    void foo(Object o) { ... }
    foo(1);// 自动装箱成 Integer
  • 放进任何接收 Object 的 API 里,比如:

    • 反射调用方法:参数列表是 Object[]
    • 日志组件:logger.info("num={}", num);
  • 做为字段、属性,被反射框架统一处理(Spring、MyBatis 等)

基本类型在这些能力上是“残废”的,只能依靠包装类把它们“包”成对象再玩。

为什么还要保留int类型

基本类型数据在读写效率方面,要比包装类高效。除此之外,在64位JVM上,在开启引用压缩的情况下,一个Integer对象占用16个字节的内存空间,而一个int类型数据只占用4字节的内存空间,前者对空间的占用是后者的4倍。

也就是说,不管是读写效率,还是存储效率,基本类型都比包装类高效。

包装类的缓存池机制

Java的Integer类内部实现了一个静态缓存池,用于存储特定范围内的整数值对应的Integer对象。

默认情况下,这个范围是-128至127。当通过Integer.valueOf(int)方法创建一个在这个范围内的整数对象时,并不会每次都生成新的对象实例,而是复用缓存中的现有对象,会直接从内存中取出,不需要新建一个对象。