前言:Java基础知识与面试经典题的探索之旅
全网最有趣通俗的Java讲解
无论你是刚入门的编程新手,还是正在准备面试的职场新人,亦或是想要巩固基础的老手,这篇文章都将为你提供有价值的见解和实用的技巧。让我们一起开启这场Java基础知识与面试经典题的探索之旅吧!
final
关键字:Java世界的“终极束缚令”
在Java的世界里,final
关键字就像是一位严格的老师,或者是你的老妈,总是在关键时刻告诉你:“别碰!这是固定的!”它的作用就是限制代码的变化,防止某些东西被随意修改。听起来有点霸道?但有时候,这种“霸道”确实能让你的代码更安全、更稳定。让我们一起来看看final
到底有多“终极”。
1. final
修饰类:我是老大,谁也别想继承我!
如果你用final
修饰一个类,那就意味着这个类是“终极版”的,不能再被继承。换句话说,这个类就像是一座无法攀登的高山,其他类只能在山脚下仰望它。
// 这是一个final类,无法被继承
final class MyFinalClass {
void myMethod() {
System.out.println(" 我是终极类,谁也别想继承我!");
}
}
// 错误示例:尝试继承final类
class MySubClass extends MyFinalClass { // 编译错误!
// ...
}
解读:
- 如果你有一个类的核心逻辑不想被其他类篡改或扩展,就可以把它设为
final
。 - 比如说,Java中的
String
类就是一个final
类。试想一下,如果有人能继承String
并随意修改它的行为,那岂不是天下大乱?
2. final
修饰方法:我是“定制版”,你不能随便改!
如果你用final
修饰一个方法,那就意味着这个方法是“定制版”的,子类不能通过重写(override)来修改它的行为。不过,子类仍然可以通过重载(overload)来扩展功能。
class MyClass {
// 这是一个final方法,子类不能重写它
final void myFinalMethod() {
System.out.println(" 我是final方法,你不能改我!");
}
}
class MySubClass extends MyClass {
// 错误示例:尝试重写final方法
void myFinalMethod() { // 编译错误!
System.out.println(" 我试图改你,但失败了...");
}
// 正确示例:通过重载扩展功能
void myFinalMethod(int x) {
System.out.println(" 我是重载版本,和你没关系!");
}
}
解读:
- 如果你有一个方法的核心逻辑不想被子类随意修改,就可以把它设为
final
。 - 比如说,Java中的
Object
类中的finalize()
方法就是一个final
方法。虽然你可以重载它,但不能重写它。
3. final
修饰变量:一旦赋值,永不更改!
如果你用final
修饰一个变量,那就意味着这个变量一旦被赋值,就不能再被修改。它就像是一块被封存的化石,永远定格在那一刻。
3.1 final
修饰成员变量
对于成员变量来说,final
变量可以在声明时赋值,或者在构造器中赋值。但一旦赋值后,就不能再修改。
class MyClass {
// 声明时赋值
final int myConst = 10;
// 在构造器中赋值
final String myName;
MyClass() {
myName = "我的名字";
}
// 错误示例:尝试修改final变量
void changeValue() {
myConst = 20; // 编译错误!
myName = "另一个名字"; // 编译错误!
}
}
解读:
- 如果你有一个变量的值在程序运行期间不应该被修改(比如常量),就可以把它设为
final
。 - 比如说,圆周率π就是一个典型的
final
变量。
3.2 final
修饰局部变量
对于局部变量来说,final
变量必须在声明时或第一次使用时赋值。
void myMethod() {
// 声明时赋值
final int x = 10;
// 在第一次使用时赋值
final int y;
y = 20;
// 错误示例:未赋值就使用
final int z; // 编译错误!
z = 30;
}
解读:
- 如果你有一个局部变量的值在方法执行期间不应该被修改,就可以把它设为
final
。 - 比如说,在一个计算方法中,某个关键参数一旦传入就不能被修改。
4. final
修饰基本类型和引用类型:区别对待!
4.1 基本类型
对于基本类型来说,final
变量一旦赋值后,其值就不能再被修改。
void myMethod() {
final int x = 10;
x = 20; // 编译错误!
}
解读:
- 基本类型的
final
变量就像是一块被锁住的硬盘,数据一旦写入就不能被覆盖。
4.2 引用类型
对于引用类型来说,final
变量一旦指向一个对象后,就不能再指向另一个对象。但对象本身的内容是可以被修改的。
class MyClass {
int value;
MyClass(int value) {
this.value = value;
}
}
void myMethod() {
final MyClass obj = new MyClass(10);
// 错误示例:尝试让obj指向另一个对象
obj = new MyClass(20); // 编译错误!
// 正确示例:修改对象的内容
obj.value = 20;
}
解读:
- 引用类型的
final
变量就像是一条拴狗的链子。狗可以到处跑(对象内容可以被修改),但链子不能拴到另一只狗上(引用不能指向另一个对象)。
为什么局部内部类和匿名内部类只能访问局部final变量?——Java世界的“借书规则”
在Java的世界里,内部类和外部类就像一对“借书”的关系。外部类就像图书馆,内部类就像一个借书的人。而局部变量就像是图书馆里的书籍。为了让借书的人(内部类)能够顺利地借阅书籍(访问变量),Java有一套严格的“借书规则”。其中一条规则就是:内部类只能访问局部final变量。这背后到底有什么玄机呢?让我们一起来揭开这个谜团!
1. 内部类和外部类的“借书”关系
首先,我们需要明确一点:内部类和外部类是平级的。即使内部类定义在方法中,它也不会随着方法的执行完毕而被销毁。相反,内部类对象可能会存活很长时间,甚至比外部类的方法执行时间更长。
举个例子:
public class Outer {
public void myMethod() {
class Inner {
void print() {
System.out.println(" 我是内部类!");
}
}
Inner inner = new Inner();
// inner仍然存活...
}
}
在这个例子中,Inner
是一个局部内部类。即使myMethod()
执行完毕,inner
对象仍然可能存活(只要没有被垃圾回收)。这就意味着,Inner
对象可能会在myMethod()
结束后继续访问Outer
类中的某些变量。
2. 局部变量的“生命周期”问题
现在问题来了:局部变量(比如myVar
)的生命周期是有限的。一旦myMethod()
执行完毕,myVar
就会被销毁。但如果Inner
对象仍然存活,并且试图访问已经被销毁的myVar
,会发生什么呢?
答案是:会出现内存访问错误。因为myVar
已经不在内存中了,而Inner
对象还在试图访问它。
为了防止这种情况发生,Java采取了一种“预防措施”:将局部变量复制一份作为内部类的成员变量。这样,即使myMethod()
执行完毕,myVar
被销毁了,内部类仍然可以访问它自己的那份“拷贝”。
3. 为什么要用final
?
到这里,问题又来了:如果我们将局部变量复制给内部类,那么如何保证这两份变量是一致的呢?比如,如果我们在外部方法中修改了myVar
,而内部类中的“拷贝”却没有同步更新,该怎么办?
这就是final
关键字派上用场的时候了!通过将局部变量声明为final
,我们可以确保:
- 变量一旦赋值后就不能被修改。
- 内部类中的“拷贝”和外部方法中的变量始终保持一致。
换句话说,final
关键字就像是给局部变量戴上了一副“手铐”,防止它在被复制后被随意修改。这样一来,内部类中的“拷贝”就永远和外部方法中的变量保持一致。
4. 代码示例:为什么只能访问final
变量?
让我们通过一个例子来理解这一点:
public class Outer {
public void myMethod() {
final int localVar = 10; // 声明为final
class Inner {
void print() {
System.out.println(" localVar = " + localVar); // 访问 localVar
}
}
Inner inner = new Inner();
inner.print(); // 输出:localVar = 10
}
}
在这个例子中:
localVar
被声明为final
,这意味着它一旦赋值后就不能被修改。- 内部类
Inner
可以访问localVar
,并且会将它的值复制到自己的成员变量中。 - 即使
myMethod()
执行完毕,localVar
被销毁了,Inner
对象仍然可以访问它自己的那份“拷贝”。
5. 为什么只能访问局部final变量?
总结一下:
- 内部类需要访问局部变量的“拷贝”:因为局部变量可能会在方法执行完毕后被销毁。
- 为了保证一致性:必须确保外部方法中的变量和内部类中的“拷贝”始终一致。
final
关键字解决了这个问题:通过禁止对局部变量的修改,确保两份变量永远不会不一致。
这就是为什么局部内部类和匿名内部类只能访问局部final变量的原因!
6. 编译后生成的两个.class
文件
最后,我们来解释一下为什么编译后会生成两个.class
文件:Test.class
和Test$1.class
。
Test.class
是外部类的字节码文件。Test$1.class
是内部类的字节码文件($符号表示这是一个内部类)。
这两个文件是平级的,彼此之间没有依赖关系。内部类有自己的生命周期和内存空间,但它仍然可以访问外部类的一些成员(比如局部final变量)。
总结:为什么要这么设计?
Java的设计者们在设计内部类时,不得不在灵活性和安全性之间做出妥协。通过限制内部类只能访问局部final变量,他们确保了:
- 内存安全:防止内部类访问已经被销毁的局部变量。
- 一致性:确保内部类和外部方法中的变量始终保持一致。
- 代码清晰:通过强制使用
final
关键字,明确告诉开发者哪些变量是“只读”的。
字符串三剑客:String、StringBuffer、StringBuilder的终极对决
在Java的世界里,字符串操作是程序员最常见的任务之一。然而,面对String
、StringBuffer
和StringBuilder
这三个看似相似却又各有千秋的类,很多新手都会感到困惑。今天,我们就来一场“终极对决”,彻底搞清楚它们之间的区别!
1. String:不可变的“倔强少年”
String
是Java中最常用的字符串类。它的特点是不可变,也就是说,一旦你创建了一个String
对象,就无法再修改它的内容。如果你尝试修改它,Java会默默地为你创建一个新的String
对象。
String str = "Hello";
str += " World"; // 这里会创建一个新的String对象
2. StringBuffer:线程安全的“肌肉猛男”
StringBuffer
是String
的可变版本。它允许你在原地修改字符串的内容,而不需要每次都创建新的对象。此外,StringBuffer
是线程安全的,这意味着它可以在多线程环境中安全使用。
StringBuffer sb = new StringBuffer("Hello");
sb.append(" World"); // 在原地修改字符串
3. StringBuilder:高性能的“闪电侠”
StringBuilder
是StringBuffer
的“轻量级”版本。它也允许在原地修改字符串的内容,但它是线程不安全的。正因为如此,StringBuilder
在单线程环境下的性能非常高。
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // 在原地修改字符串
4.性能对比
String
的性能最差,因为它每次拼接都会创建新的对象。StringBuilder
的性能最好,因为它没有同步机制的开销。StringBuffer
的性能介于两者之间。
重载与重写的“双胞胎”世界
在Java的世界里,重载(Overloading)和重写(Overriding)就像是双胞胎,名字相近,但性格却大不相同。它们都是用来增加代码灵活性和复用性的工具,但适用的场景和规则却截然不同。今天,我们就来揭开这对“双胞胎”的神秘面纱!
1. 重载(Overloading):同一个类中的“多面手”
重载就像是一个“多面手”,它允许你在同一个类中定义多个方法,它们的名字相同,但参数列表不同。这就好比一个人会多种技能,可以根据不同的场合选择不同的技能来应对。
重载的特点:
- 方法名相同:必须使用相同的名称。
- 参数不同:参数的类型、个数或顺序必须不同。
- 返回值和访问修饰符可以不同:只要参数不同,返回值和访问修饰符可以自由变化。
- 发生在编译时:编译器根据参数列表来决定调用哪个方法。
public class Calculator {
// 两个参数的加法
public int add(int a, int b) {
return a + b;
}
// 三个参数的加法
public int add(int a, int b, int c) {
return a + b + c;
}
// 不同类型的参数
public double add(double a, double b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(1, 2)); // 调用两个int参数的方法
System.out.println(calc.add(1, 2, 3)); // 调用三个int参数的方法
System.out.println(calc.add(1.5, 2.5)); // 调用两个double参数的方法
}
}
2. 重写(Overriding):子类中的“个性表达”
重写更像是子类对父类的一种“个性表达”。它允许子类重新定义父类中的方法,从而实现不同的行为。这就好比孩子继承了父母的基因,但又有自己的独特之处。
重写的特点:注意区分重载!!
- 方法名和参数列表必须相同:子类的方法必须与父类的方法完全匹配。
- 返回值范围小于等于父类:子类的返回值类型必须与父类相同,或者更具体(更窄)。
- 抛出的异常范围小于等于父类:子类不能抛出比父类更广泛的异常。
- 访问修饰符范围大于等于父类:子类的方法访问修饰符必须与父类相同,或者更宽松(更宽)。
- 不能重写private方法:如果父类的方法是
private
的,子类无法重写它。
class Animal {
// 父类方法
public void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
// 子类重写父类方法
@Override
public void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
// 子类重写父类方法
@Override
public void sound() {
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
animal.sound(); // 输出:Animal makes a sound
Dog dog = new Dog();
dog.sound(); // 输出:Dog barks
Cat cat = new Cat();
cat.sound(); // 输出:Cat meows
}
}
3. 重载与重写的“终极对比”
特性 | 重载(Overloading) | 重写(Overriding) |
---|---|---|
发生场景 | 同一个类中 | 父类和子类之间 |
方法名 | 必须相同 | 必须相同 |
参数列表 | 必须不同 | 必须相同 |
返回值类型 | 可以不同 | 必须相同或更具体 |
异常范围 | 无限制 | 必须相同或更狭窄 |
访问修饰符 | 可以不同 | 必须相同或更宽松 |
实例调用 | 编译时多态 | 运行时多态 |
关注与订阅
希望这篇博客能为你在Java学习和职业发展中提供有力的支持!如果你觉得这篇文章对你有帮助,或者你对Java编程还有其他疑问,欢迎在评论区留言交流。你的反馈是我不断进步的动力!
如果你喜欢我的内容,也欢迎关注我的博客或社交媒体账号。我会持续分享更多关于Java基础知识、面试经典题以及更多有趣的技术干货。让我们一起在编程的世界中探索更多的奥秘!
最后,如果你觉得这篇文章值得一看,不妨点个赞,让更多人看到!你的支持是我创作的最大动力!谢谢!