一、Java概述1、什么是Java Java 是一门面向对象的编程语言,由 Sun 公司的詹姆斯·高斯林团队于 1995 年推出。吸收了 C++ 语言中大量的优点,但又抛弃了 C++ 中容易出错的地方,如垃圾回收、指针。
同时,Java 又是一门平台无关的编程语言,即一次编译,处处运行。
只需要在对应的平台上安装 JDK,就可以实现跨平台,在 Windows、macOS、Linux 操作系统上运行
2、Java语言有哪些特点Java 语言的特点有:
①、面向对象,主要是封装,继承,多态。
②、平台无关性,“一次编写,到处运行”,因此采用 Java 语言编写的程序具有很好的可移植性。
③、支持多线程。C++ 语言没有内置的多线程机制,因此必须调用操作系统的 API 来完成多线程程序设计,而 Java 却提供了封装好多线程支持;
④、支持 JIT 编译,也就是即时编译器,它可以在程序运行时将字节码转换为热点机器码来提高程序的运行速度。
3、JVM、JDK和JRE有什么区别?JVM:也就是 Java 虚拟机,是 Java 实现跨平台的关键所在,不同的操作系统有不同的 JVM 实现。JVM 负责将 Java 字节码转换为特定平台的机器码,并执行。
JRE:也就是 Java 运行时环境,包含了运行 Java 程序所必需的库,以及 JVM。
JDK:一套完整的 Java SDK,包括 JRE,编译器 javac、Java 文档生成工具 javadoc、Java 字节码工具 javap 等。为开发者提供了开发、编译、调试 Java 程序的一整套环境。
简单来说,JDK 包含 JRE,JRE 包含 JVM。
4、什么是字节码?采用字节码的好处是什么?Java 程序从源代码到运行需要经过三步:
编译:将源代码文件 .java 编译成 JVM 可以识别的字节码文件 .class解释:JVM 执行字节码文件,将字节码翻译成操作系统能识别的机器码执行:操作系统执行二进制的机器码5、为什么说Java是“编译与解释并存”的语言?编译型语言是指编译器针对特定的操作系统,将源代码一次性翻译成可被该平台执行的机器码。
解释型语言是指解释器对源代码进行逐行解释,解释成特定平台的机器码并执行。
举个例子,我想读一本国外的小说,我有两种选择:
找个翻译,等翻译将小说全部都翻译成汉语,一次性读完。找个翻译,翻译一段我读一段,慢慢把书读完。 之所以有人说 Java 是“编译与解释并存”的语言,是因为 Java 程序需要先将 Java 源代码文件编译字节码文件,再解释执行。
二、基础语法1、Java有哪些数据类型?Java 的数据类型可以分为两种:基本数据类型和引用数据类型。
它们的默认值和占用大小如下所示:
对于引用数据类型而言,他们的大小是不固定的,会受到操作系统位数的影响:
在 32 位 JVM 中:一个对象引用通常占用 4 个字节。在 64 位 JVM 中: 默认情况下,一个对象引用占用 8 个字节。给Integer最大值+1,是什么结果?
当给 Integer.MAX_VALUE 加 1 时,会发生溢出,变成 Integer.MIN_VALUE。
代码语言:javascript复制int maxValue = Integer.MAX_VALUE;
System.out.println("Integer.MAX_VALUE = " + maxValue); // Integer.MAX_VALUE = 2147483647
System.out.println("Integer.MAX_VALUE + 1 = " + (maxValue + 1)); // Integer.MAX_VALUE + 1 = -2147483648
// 用二进制来表示最大值和最小值
System.out.println("Integer.MAX_VALUE in binary: " + Integer.toBinaryString(maxValue)); // Integer.MAX_VALUE in binary: 1111111111111111111111111111111
System.out.println("Integer.MIN_VALUE in binary: " + Integer.toBinaryString(Integer.MIN_VALUE)); // Integer.MIN_VALUE in binary: 10000000000000000000000000000000这是因为 Java 的整数类型采用的是二进制补码表示法,溢出时值会变成最小值。
Integer.MAX_VALUE 的二进制表示是 01111111 11111111 11111111 11111111(32 位)。加 1 后结果变成 10000000 00000000 00000000 00000000,即 -2147483648(Integer.MIN_VALUE)。2、自动类型转换和强制类型转换 当把一个范围较小的数值或变量赋给另外一个范围较大的变量时,会进行自动类型转换;反之,需要强制转换。
①、float f=3.4,对吗?
不正确。3.4 默认是双精度,将双精度赋值给浮点型属于下转型(down-casting,也称窄化)会造成精度丢失,因此需要强制类型转换float f =(float)3.4;或者写成float f =3.4F
②、short s1 = 1; s1 = s1 + 1;对吗?short s1 = 1; s1 += 1;对吗?
short s1 = 1; s1 = s1 + 1; 会编译出错,由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。
而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1); 其中有隐含的强制类型转换。
3、break,continue,return 的区别及作用?break 跳出整个循环,不再执行循环(结束当前的循环体)continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)4、float 是怎么表示小数的?float 表示小数的方式是基于 IEEE 754 标准的,采用二进制浮点数格式。
S:符号位,0 代表正数,1 代表负数;M:尾数部分,用于表示数值的精度;比如说 1.25∗22;1.25 就是尾数;R:基数,十进制中的基数是 10,二进制中的基数是 2;E:指数部分,例如 10−1 中的 -1 就是指数。 这种表示方法可以将非常大或非常小的数值用有限的位数表示出来,但这也意味着可能会有精度上的损失。
单精度浮点数占用 4 字节(32 位),这 32 位被分为三个部分:符号位、指数部分和尾数部分。
kaito:浮点数 1 位符号位、8 位指数位、23 位尾数位。符号位决定正负,0 表示正数,1 表示负数。指数位存储的是偏置后的指数,实际指数要减去 127。尾数位存储的是小数部分的二进制表示。
按照这个规则,将十进制数 25.125 转换为浮点数,转换过程是这样的:
整数部分:25 转换为二进制是 11001;小数部分:0.125 转换为二进制是 0.001;用二进制科学计数法表示:25.125 = 1.001001×24符号位 S 是 0,表示正数;指数部分 E 是 4,转换为二进制是 100;尾数部分 M 是 1.001001。
kaito:25.125 使用浮点数时需要注意,由于精度的限制,进行数学运算时可能会遇到舍入误差,特别是连续运算累积误差可能会变得显著。
4、数据准确性高是怎么保证的? 在金融计算中,保证数据准确性有两种方案,一种使用 BigDecimal,一种将浮点数转换为整数 int 进行计算。
肯定不能使用 float 和 double 类型,它们无法避免浮点数运算中常见的精度问题,因为这些数据类型采用二进制浮点数来表示,无法准确地表示,例如 0.1。
代码语言:javascript复制BigDecimal num1 = new BigDecimal("0.1");
BigDecimal num2 = new BigDecimal("0.2");
BigDecimal sum = num1.add(num2);
System.out.println("Sum of 0.1 and 0.2 using BigDecimal: " + sum); // 输出 0.3,精确计算 在处理小额支付或计算时,通过转换为较小的货币单位(如分),这样不仅提高了运算速度,还保证了计算的准确性。
代码语言:javascript复制int priceInCents = 199; // 商品价格199分
int quantity = 3;
int totalInCents = priceInCents * quantity; // 计算总价
System.out.println("Total price in cents: " + totalInCents); // 输出597分三、面向对象1、面向对象和面向过程的区别? 面向过程是以过程为核心,通过函数完成任务,程序结构是函数+步骤组成的顺序流程。
面向对象是以对象为核心,通过对象交互完成任务,程序结构是类和对象组成的模块化结构,代码可以通过继承、组合、多态等方式复用。
2、面向对象编程有哪些特性?面向对象编程有三大特性:封装、继承、多态。
封装是什么?
封装是指将数据(属性,或者叫字段)和操作数据的方法(行为)捆绑在一起,形成一个独立的对象(类的实例)。
代码语言:javascript复制class Nvshen {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
}可以看得出,女神类对外没有提供 age 的 getter 方法,因为女神的年龄要保密。
所以,封装是把一个对象的属性私有化,同时提供一些可以被外界访问的方法。
继承是什么?
继承允许一个类(子类)继承现有类(父类或者基类)的属性和方法。以提高代码的复用性,建立类之间的层次关系。
同时,子类还可以重写或者扩展从父类继承来的属性和方法,从而实现多态。
代码语言:javascript复制class Person {
protected String name;
protected int age;
public void eat() {
System.out.println("吃饭");
}
}
class Student extends Person {
private String school;
public void study() {
System.out.println("学习");
}
} Student 类继承了 Person 类的属性(name、age)和方法(eat),同时还有自己的属性(school)和方法(study)。
多态是什么?
多态允许不同类的对象对同一消息做出响应,但表现出不同的行为(即方法的多样性)。
多态其实是一种能力——同一个行为具有不同的表现形式;换句话说就是,执行一段代码,Java 在运行时能根据对象类型的不同产生不同的结果。
多态的前置条件有三个:
子类继承父类子类重写父类的方法父类引用指向子类的对象代码语言:javascript复制//子类继承父类
class Wangxiaoer extends Wanger {
public void write() { // 子类重写父类方法
System.out.println("记住仇恨,表明我们要奋发图强的心智");
}
public static void main(String[] args) {
// 父类引用指向子类对象
Wanger wanger = new Wangxiaoer();
wanger.write();
}
}
class Wanger {
public void write() {
System.out.println("沉默王二是沙雕");
}
}为什么Java里面要多组合少继承?
继承适合描述“is-a”的关系,但继承容易导致类之间的强耦合,一旦父类发生改变,子类也要随之改变,违背了开闭原则(尽量不修改现有代码,而是添加新的代码来实现)。
组合适合描述“has-a”或“can-do”的关系,通过在类中组合其他类,能够更灵活地扩展功能。组合避免了复杂的类继承体系,同时遵循了开闭原则和松耦合的设计原则。
举个例子,假设我们采用继承,每种形状和样式的组合都会导致类的急剧增加:
代码语言:javascript复制// 基类
class Shape {
public void draw() {
System.out.println("Drawing a shape");
}
}
// 圆形
class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}
// 带红色的圆形
class RedCircle extends Circle {
@Override
public void draw() {
System.out.println("Drawing a red circle");
}
}
// 带绿色的圆形
class GreenCircle extends Circle {
@Override
public void draw() {
System.out.println("Drawing a green circle");
}
}
// 类似的,对于矩形也要创建多个类
class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
class RedRectangle extends Rectangle {
@Override
public void draw() {
System.out.println("Drawing a red rectangle");
}
}组合模式更加灵活,可以将形状和颜色分开,松耦合。
代码语言:javascript复制// 形状接口
interface Shape {
void draw();
}
// 颜色接口
interface Color {
void applyColor();
}形状干形状的事情。
代码语言:javascript复制// 圆形的实现
class Circle implements Shape {
private Color color; // 通过组合的方式持有颜色对象
public Circle(Color color) {
this.color = color;
}
@Override
public void draw() {
System.out.print("Drawing a circle with ");
color.applyColor(); // 调用颜色的逻辑
}
}
// 矩形的实现
class Rectangle implements Shape {
private Color color;
public Rectangle(Color color) {
this.color = color;
}
@Override
public void draw() {
System.out.print("Drawing a rectangle with ");
color.applyColor();
}
}颜色干颜色的事情。
代码语言:javascript复制// 红色的实现
class RedColor implements Color {
@Override
public void applyColor() {
System.out.println("red color");
}
}
// 绿色的实现
class GreenColor implements Color {
@Override
public void applyColor() {
System.out.println("green color");
}
}3、多态的实现原理? 多态通过动态绑定实现,Java 使用虚方法表存储方法指针,方法调用时根据对象实际类型从虚方法表查找具体实现。
4、重载与重写的区别? 如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载。
如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体不同),我们称之为方法重写。
方法重载发生在同一个类中,同名的方法如果有不同的参数(参数类型不同、参数个数不同或者二者都不同)。方法重写发生在子类与父类之间,要求子类与父类具有相同的返回类型,方法名和参数列表,并且不能比父类的方法声明更多的异常。5、访问修饰符 public、private、protected、以及默认时的区别?Java 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。private : 在同一类内可见。可以修饰变量、方法。注意:不能修饰类(外部类)public : 对所有类可见。可以修饰类、接口、变量、方法protected : 对同一包内的类和所有子类可见。可以修饰变量、方法。注意:不能修饰类(外部类)6、this关键字有什么作用?this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this 的用法在 Java 中大体可以分为 3 种:
1)普通的直接引用,this 相当于是指向当前对象本身
2)形参与成员变量名字重名,用 this 来区分:
代码语言:javascript复制public Person(String name,int age){
this.name=name;
this.age=age;
}3)引用本类的构造方法
7、抽象类和接口有什么区别? 抽象类符合 is-a 的关系,而接口更像是 has-a 的关系,比如说一个类可以序列化的时候,它只需要实现 Serializable 接口就可以了,不需要去继承一个序列化类。
抽象类更多地是用来为多个相关的类提供一个共同的基础框架,包括状态的初始化,而接口则是定义一套行为标准,让不同的类可以实现同一接口,提供行为的多样化实现。
继承与抽象的区别?
继承是一种允许子类继承父类属性和方法的机制。通过继承,子类可以重用父类的代码。
抽象是一种隐藏复杂性和只显示必要部分的技术。在面向对象编程中,抽象可以通过抽象类和接口实现。
抽象类和普通类的区别
抽象类使用 abstract 关键字定义,不能被实例化,只能作为其他类的父类。普通类没有 abstract 关键字,可以直接实例化。
抽象类可以包含抽象方法和非抽象方法。抽象方法没有方法体,必须由子类实现。普通类只能包含非抽象方法。
代码语言:javascript复制abstract class Animal {
// 抽象方法
public abstract void makeSound();
// 非抽象方法
public void eat() {
System.out.println("This animal is eating.");
}
}
class Dog extends Animal {
// 实现抽象方法
@Override
public void makeSound() {
System.out.println("Woof");
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
dog.makeSound(); // 输出 "Woof"
dog.eat(); // 输出 "This animal is eating."
}
}8、成员变量与局部变量的区别有哪些?1)从语法形式上看:成员变量是属于类的,⽽局部变量是在⽅法中定义的变量或是⽅法的参数;成员变量可以被 public , private , static 等修饰符所修饰,⽽局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
2)从变量在内存中的存储⽅式来看:如果成员变量是使⽤ static 修饰的,那么这个成员变量是属于类的,如果没有使⽤ static 修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引⽤数据类型,那存放的是指向堆内存对象的引⽤或者是指向常量池中的地址。
3)从变量在内存中的⽣存时间上看:成员变量是对象的⼀部分,它随着对象的创建⽽存在,⽽局部变量随着⽅法的调⽤⽽⾃动消失。
4)成员变量如果没有被赋初值:则会⾃动以类型的默认值⽽赋值(⼀种情况例外:被 final 修饰的成员变量也必须显式地赋值),⽽局部变量则不会⾃动赋值
9、static关键字static 关键字可以用来修饰变量、方法、代码块和内部类,以及导入包。
10、final关键字有什么作用?①、当 final 修饰一个类时,表明这个类不能被继承。比如,String 类、Integer 类和其他包装类都是用 final 修饰的。
二哥的 Java 进阶之路:final 修饰类②、当 final 修饰一个方法时,表明这个方法不能被重写(Override)。也就是说,如果一个类继承了某个类,并且想要改变父类中被 final 修饰的方法的行为,是不被允许的。
③、当 final 修饰一个变量时,表明这个变量的值一旦被初始化就不能被修改。
如果是基本数据类型的变量,其数值一旦在初始化之后就不能更改;如果是引用类型的变量,在对其初始化之后就不能再让其指向另一个对象。
二哥的 Java 进阶之路:不能更改但是引用指向的对象内容可以改变。
11、== 和 equals 的区别?在 Java 中,== 操作符和 equals() 方法用于比较两个对象:
① ==:用于比较两个对象的引用,即它们是否指向同一个对象实例。
如果两个变量引用同一个对象实例,== 返回 true,否则返回 false。
对于基本数据类型(如 int, double, char 等),== 比较的是值是否相等。
② equals() 方法:用于比较两个对象的内容是否相等。默认情况下,equals() 方法的行为与 == 相同,即比较对象引用,如在超类 Object 中:
代码语言:javascript复制public boolean equals(Object obj) {
return (this == obj);
} 然而,equals() 方法通常被各种类重写。例如,String 类重写了 equals() 方法,以便它可以比较两个字符串的字符内容是否完全一样。
二哥的 Java 进阶之路,String的equals()源码举个例子:
代码语言:javascript复制String a = new String("沉默王二");
String b = new String("沉默王二");
// 使用 == 比较
System.out.println(a == b); // 输出 false,因为 a 和 b 引用不同的对象
// 使用 equals() 比较
System.out.println(a.equals(b)); // 输出 true,因为 a 和 b 的内容相同12、为什么重写 equals 时必须重写 hashcode 方法? 因为基于哈希的集合类(如 HashMap)需要基于这一点来正确存储和查找对象。
具体地说,HashMap 通过对象的哈希码将其存储在不同的“桶”中,当查找对象时,它需要使用 key 的哈希码来确定对象在哪个桶中,然后再通过 equals() 方法找到对应的对象。
如果重写了 equals()方法而没有重写 hashCode()方法,那么被认为相等的对象可能会有不同的哈希码,从而导致无法在 HashMap 中正确处理这些对象。
什么是hashCode方法
hashCode() 方法的作⽤是获取哈希码,它会返回⼀个 int 整数,定义在 Object中, 是一个本地⽅法。
代码语言:javascript复制public native int hashCode();为什么要有hashCode方法
hashCode 方法主要用来获取对象的哈希码,哈希码是由对象的内存地址或者对象的属性计算出来的,它是⼀个 int 类型的整数,通常是不会重复的,因此可以用来作为键值对的建,以提高查询效率。
例如HashMap中的 key 就是通过 hashCode 来实现的,通过调用 hashCode 方法获取键的哈希码,并将其与右移 16 位的哈希码进行异或运算。
代码语言:javascript复制static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}为什么两个对象有相同的 hashcode 值,它们也不一定相等?
这主要是由于哈希码(hashCode)的本质和目的所决定的。
哈希码是通过哈希函数将对象中映射成一个整数值,其主要目的是在哈希表中快速定位对象的存储位置。
由于哈希函数将一个较大的输入域映射到一个较小的输出域,不同的输入值(即不同的对象)可能会产生相同的输出值(即相同的哈希码)。
这种情况被称为哈希冲突。当两个不相等的对象发生哈希冲突时,它们会有相同的 hashCode。
为了解决哈希冲突的问题,哈希表在处理键时,不仅会比较键对象的哈希码,还会使用 equals 方法来检查键对象是否真正相等。如果两个对象的哈希码相同,但通过 equals 方法比较结果为 false,那么这两个对象就不被视为相等。
代码语言:javascript复制if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;hashCode 和 equals 方法的关系?
如果两个对象通过 equals 相等,它们的 hashCode 必须相等。否则会导致哈希表类数据结构(如 HashMap、HashSet)的行为异常。
在哈希表中,如果 equals 相等但 hashCode 不相等,哈希表可能无法正确处理这些对象,导致重复元素或键值冲突等问题。