Java基础与面试-每日必看(2)

前言: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,我们可以确保:

  1. 变量一旦赋值后就不能被修改
  2. 内部类中的“拷贝”和外部方法中的变量始终保持一致

换句话说,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变量,他们确保了:

  1. 内存安全:防止内部类访问已经被销毁的局部变量。
  2. 一致性:确保内部类和外部方法中的变量始终保持一致。
  3. 代码清晰:通过强制使用final关键字,明确告诉开发者哪些变量是“只读”的。


字符串三剑客:String、StringBuffer、StringBuilder的终极对决

在Java的世界里,字符串操作是程序员最常见的任务之一。然而,面对StringStringBufferStringBuilder这三个看似相似却又各有千秋的类,很多新手都会感到困惑。今天,我们就来一场“终极对决”,彻底搞清楚它们之间的区别!


1. String:不可变的“倔强少年”

  String是Java中最常用的字符串类。它的特点是不可变也就是说,一旦你创建了一个String对象,就无法再修改它的内容。如果你尝试修改它,Java会默默地为你创建一个新的String对象。

String str = "Hello";
str += " World"; // 这里会创建一个新的String对象 

2. StringBuffer:线程安全的“肌肉猛男”

StringBufferString的可变版本。它允许你在原地修改字符串的内容,而不需要每次都创建新的对象。此外,StringBuffer线程安全的,这意味着它可以在多线程环境中安全使用。

StringBuffer sb = new StringBuffer("Hello");
sb.append("  World"); // 在原地修改字符串 

3. StringBuilder:高性能的“闪电侠”

StringBuilderStringBuffer的“轻量级”版本。它也允许在原地修改字符串的内容,但它是线程不安全的。正因为如此,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基础知识、面试经典题以及更多有趣的技术干货。让我们一起在编程的世界中探索更多的奥秘!

最后,如果你觉得这篇文章值得一看,不妨点个赞,让更多人看到!你的支持是我创作的最大动力!谢谢! 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Starry-Walker

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值