Java基础知识(二)

自动装箱与拆箱?

装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;

成员变量与局部变量的区别有哪些?

语法形式上的差异
成员变量:作为类的一部分,它们可以被public、private、protected或static等修饰符修饰,以控制其访问权限或表示其属于类本身而非类的实例。
局部变量:仅在方法内部定义,可以是方法的参数或在方法体内声明的变量。它们不能被访问控制修饰符(如public、private)或static修饰。然而,它们都可以被final修饰,表示一旦赋值便不可更改。
内存存储与管理的不同
成员变量(非static):属于类的实例,存储在堆内存中,随着对象的创建而分配空间,对象销毁时释放空间。
成员变量(static):属于类本身,同样存储在堆内存中,但独立于任何对象实例。所有实例共享同一个static成员变量。
局部变量:存储在栈内存中,它们的生命周期仅限于方法调用的过程中。方法执行完毕,局部变量随之消失,其占用的栈空间被释放。
生存时间的对比
成员变量:其生存时间与对象的生存时间相同,即对象创建时成员变量被初始化,对象销毁时成员变量也随之销毁。
局部变量:仅在方法被调用时存在,方法执行完毕后自动消失。
初始化规则的差异
成员变量:若未显式初始化,则Java会自动为它们赋予类型的默认值(如int为0,boolean为false等)。但需要注意的是,被final修饰的成员变量必须显式初始化,无论是直接初始化还是在构造器中初始化。
局部变量:必须在使用前显式初始化,否则编译器会报错,因为它们不会自动获得默认值。

静态方法和实例方法有何不同?

调用方式

静态方法:可以直接通过类名来调用,无需创建类的实例。这种调用方式使得静态方法非常适合作为工具类或辅助类中的方法,因为它们不依赖于任何特定的对象实例。
实例方法:必须先创建类的实例(对象),然后通过这个实例来调用。实例方法与对象的状态紧密相关,因为它们可以访问和修改对象的属性。

访问权限

静态方法:只能访问类的静态成员(包括静态变量和静态方法),而不能直接访问类的实例成员(非静态变量和非静态方法)。如果静态方法需要访问实例成员,它必须先创建类的实例。
实例方法:可以访问类的静态成员和实例成员。由于实例方法是与对象关联的,因此它们可以自由地访问和修改对象的属性。

生命周期与内存分配

静态方法:它们的生命周期与类的生命周期相同,从类被加载到JVM(Java虚拟机)开始,直到类被卸载。静态方法在类加载时就已经分配了内存,并在整个应用程序的生命周期内保持有效。
实例方法:它们的生命周期与对象的生命周期相同。每当创建类的实例时,都会为实例方法分配内存。当对象被销毁时,与之关联的实例方法也不再可用。

关联性与数据访问

静态方法:与类相关联,而不是与特定的对象实例相关联。因此,它们通常用于执行与类相关的操作,如数据转换、验证或类级别的计算,这些操作不依赖于任何特定的对象状态。
实例方法:与对象实例紧密相关。它们用于操作对象的状态,包括访问和修改对象的属性。实例方法通常表示对象的行为和操作。

参数传递

静态方法:其参数通常是通过参数列表传递的,与对象状态无关。
实例方法:虽然其参数也是通过参数列表传递的,但实例方法本身可以通过this关键字(在Java中)或self参数(在其他一些语言中)访问对象的属性和其他实例方法。

多态性与继承

实例方法:支持多态性和继承。子类可以重写父类的实例方法,从而改变方法的行为。
静态方法:不具有多态性,因为它们属于类本身,而不是任何特定的对象实例。此外,静态方法也不能被重写(但可以被隐藏),因为静态解析发生在编译时,而不是运行时。

使用场景

静态方法:适用于不需要访问对象状态的场景,如工具函数、辅助函数或执行类级别的计算。
实例方法:适用于需要操作对象状态或表示对象行为的场景。

== 与 equals

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型比较的是内存地址)。
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

字符串常量池

在 Java 中,字符串常量池(String Constant Pool)是一个特殊的存储区域,用于存储字符串常量。当你使用双引号("")直接声明一个字符串时,JVM 会首先检查字符串常量池中是否已经存在该字符串。如果存在,就返回常量池中该字符串的引用;如果不存在,就在常量池中创建一个新的字符串,并返回其引用。
这就是为什么在你的例子中:

String aa = "ab"; // 放在常量池中  
String bb = "ab"; // 从常量池中查找,找到相同的字符串,因此 aa 和 bb 引用相同的对象  
if (aa == bb) // true,因为 aa 和 bb 引用的是常量池中的同一个对象  
    System.out.println("aa==bb");

这段代码会输出 “aa==bb”。

使用 new 关键字创建字符串对象

然而,当你使用 new 关键字来创建字符串对象时,如:

String a = new String("ab"); // 创建一个新的 String 对象,不管常量池中是否已存在 "ab"  
String b = new String("ab"); // 同样创建一个新的 String 对象,与 a 无关

这里,尽管 "ab" 字符串本身可能已经在常量池中,但 new 关键字每次都会创建一个新的 String 对象在堆上,并返回这个新对象的引用。因此,ab 引用的是堆上两个不同的 String 对象,所以 a == bfalse

equals() 方法

String 类重写了 Object 类的 equals() 方法,使其比较两个字符串的内容是否相等,而不是比较它们的引用(即内存地址)。这就是为什么:

if (a.equals(b)) // true,因为 a 和 b 的内容都是 "ab"  
    System.out.println("aEQb");

这段代码会输出 “aEQb”。

基本数据类型和自动装箱

在你的最后一个例子中:

if (42 == 42.0) { // true  
    System.out.println("true");  
}

这里发生的是自动装箱和自动拆箱,以及基本数据类型之间的转换。42 是一个 int 类型的字面量,而 42.0 是一个 double 类型的字面量。在比较时,int 类型的 42 会被自动提升(或称为自动装箱为 Integer,但在这个场景下实际上是类型提升为 double)为 double 类型的 42.0,然后进行比较。由于两个值相等,所以结果是 true。但请注意,这种比较在涉及到复杂类型(如自定义对象)时不会自动发生,你需要显式地调用 equals() 方法或进行其他类型的转换。

为什么重写 equals 时必须重写 hashCode 方法?

hashCode() 的作用

hashCode()方法的主要作用是生成一个整数值(即哈希码),这个值用于确定对象在哈希表(如HashSet、HashMap等)中的存储位置。通过减少冲突(即不同对象产生相同哈希码的情况)和加快查找速度,hashCode()方法极大地提高了基于哈希的集合的性能。

为什么需要 hashCode()

在哈希表中,直接通过键(Key)来查找值(Value)的效率是非常高的,这得益于哈希表的内部机制。哈希表通过计算键的哈希码来快速定位键可能存在的位置(即“桶”或“槽”),然后再通过equals()方法来确认是否找到了正确的键。如果没有hashCode()方法,哈希表将不得不遍历所有的键值对来查找一个特定的键,这将大大降低查找效率。

hashCode() 与 equals() 的关系

如果两个对象相等(即equals()方法返回true),则它们的hashCode()方法必须返回相同的整数值。这是hashCode()方法合同的一部分,保证了在哈希表中基于equals()方法的正确性。
如果两个对象的hashCode()方法返回不同的整数值,则这两个对象在equals()方法下一定不相等。但是,如果两个对象的hashCode()方法返回相同的整数值(即哈希码冲突),则这两个对象在equals()方法下可能相等,也可能不相等,需要进一步通过equals()方法来判断。
如果覆盖了equals()方法,则也应该覆盖hashCode()方法,以保持上述的合同关系。如果不这样做,可能会违反哈希表的假设,导致程序出现错误的行为。

默认的 hashCode() 行为

Object类的hashCode()方法默认的实现是基于对象的内存地址的(在JVM内部,对象的内存地址是唯一的,但这并不意味着它会被直接用作哈希码)。因此,如果两个对象不是同一个对象(即不是通过==比较的同一个实例),那么它们的默认哈希码通常是不同的。但是,这并不意味着默认的哈希码是有用的,特别是在需要基于对象内容进行比较的场合,如HashSet或HashMap中。

为什么 Java 中只有值传递?

在 Java 中,所有参数传递都是按值传递(Pass-by-Value)的方式进行的。这意味着无论你传递基本类型还是对象引用,传递的都是这些值的副本。下面我将解释为什么 Java 采用这种传递方式以及它的含义:

基本类型的值传递

当你传递一个基本类型的变量(如 int, double, char 等)给方法时,实际上是传递了该变量的值的一个副本。这意味着在方法内部对该值所做的任何修改都不会影响原始变量。

对象引用的值传递

当你传递一个对象引用给方法时,传递的是该引用的副本。这意味着在方法内部,你可以使用这个引用访问对象的内容并修改对象的状态,但不能改变引用本身指向的对象。也就是说,你可以修改对象内部的数据,但不能改变方法外部的引用指向另一个对象。

public class ValuePassingExample {

  public static void main(String[] args) {
    int number = 10;
    modifyNumber(number);
    System.out.println(number); // 输出 10

    MyObject obj = new MyObject(5);
    modifyObject(obj);
    System.out.println(obj.getValue()); // 输出 100
  }

  public static void modifyNumber(int num) {
    num = 20; // 修改局部变量,不影响外部变量
  }

  public static void modifyObject(MyObject obj) {
    obj.setValue(100); // 修改对象状态,不影响外部引用
  }
}

class MyObject {
  private int value;

  public MyObject(int value) {
    this.value = value;
  }

  public int getValue() {
    return value;
  }

  public void setValue(int value) {
    this.value = value;
  }
}

线程、程序、进程的基本概念。以及他们之间关系是什么?

线程

定义:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
特点
线程是轻量级的进程,它是CPU任务调度的最小单元。
线程共享所属进程的内存空间,包括数据区、代码区、堆区等。
线程间通信更简便、高效,但操作共享系统资源时也可能带来安全隐患。

程序

定义:程序是计算机指令的集合,这些指令是为了完成某个特定的任务而编写的。从根本上说,程序是由一系列指令组成,这些指令告诉计算机如何执行一个计算任务,这里的计算可以是数学运算、符号运算,也可以是声音和图像的处理。

特点:

程序是静态的,它是一段静态的代码或静态对象。
程序可以被多次执行,每次执行都形成一个独立的进程。

进程

定义:进程是程序的一次执行过程,或是一个正在运行的程序。它是系统进行资源分配和调度的一个独立单元。进程具有动态性,它包含程序的执行过程以及执行过程中所需要的系统资源(如CPU、内存、文件等)。
特点
进程具有独立的内存空间,系统中的每个进程都运行在各自独立的内存空间中。
进程是资源分配的基本单位,操作系统根据进程的需要为其分配资源。
进程是并发执行的,多个进程可以在同一时间内并发执行。

它们之间的关系

程序与进程:程序是静态的,而进程是动态的。程序是进程的基础,进程是程序的执行过程。一个程序可以对应多个进程,特别是在多实例运行或分布式系统中。
进程与线程:线程是进程的一部分,一个进程可以包含多个线程。线程是CPU调度的基本单位,而进程则是资源分配的基本单位。进程中的所有线程共享该进程的资源,如内存空间、文件描述符等。

如何在面试中-画龙点睛

String、StringBuffer、StringBuilder有什么区别?

典型回答

String是Java语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的Immutable类,被声明成为final class,所有属性也都是final的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的String对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。
StringBuffer是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用append或者add方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是StringBuilder。
StringBuilder是Java 1.5中新增的,在能力上和StringBuffer没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

考点分析

几乎所有的应用开发都离不开操作字符串,理解字符串的设计和实现以及相关工具如拼接类的使用,对写出高质量代码是非常有帮助的。关于这个问题,我前面的回答是一个通常的概要性回答,至少你要知道String是Immutable的,字符串操作不当可能会产生大量临时字符串,以及线程安全方面的区别。
如果继续深入,面试官可以从各种不同的角度考察,比如可以:
通过String和相关类,考察基本的线程安全设计与实现,各种基础编程实践。
考察JVM对象缓存机制的理解以及如何良好地使用。
考察JVM优化Java代码的一些技巧。
String相关类的演进,比如Java 9中实现的巨大变化。

点睛

结合实际使用场景,分析String、StringBuffer、StringBuilder特征及区别。

String怎么实现不可变性的
String str1 = "Hello";
String str2 = str1.replace('H', 'h'); // 创建新的String对象
System.out.println(str1); // 输出: Hello
System.out.println(str2); // 输出: hello

str1.concat(" World"); // 这里依然不会改变str1的值
System.out.println(str1); // 输出: Hello

在上述示例中:当我们尝试修改str1的内容时,无论是通过replace方法还是concat方法,都不会改变str1原本的值。replace方法返回的是一个新的String对象str2,而不是修改str1本身。这一点可以从输出结果看出,str1的内容始终保持不变。
**字符数组的私有和最终(final)修饰:**String类内部使用一个final类型的char数组(在Java 9及以后版本中为byte数组结合编码信息)来存储字符串的字符序列,由于final修饰符的作用,一旦初始化后,该数组引用不能改变,即不能指向新的字符数组。
**类的final修饰:**String类本身被声明为final,这就意味着不能有任何String的子类,确保了String类的不可变性特性不会被子类破坏。
**缺少修改方法:**String类没有提供任何修改字符串内容的方法,如replace、insert等,而是提供了创建新字符串的方法,如concat、replaceFirst等,这些方法调用后返回的是一个新的String对象,原始对象始终保持不变。
**哈希码缓存:**String类在初始化时计算并存储其哈希码值,由于字符串内容不再改变,因此哈希码也就固定下来,后续无需再次计算,提高了效率,同时也符合不可变性要求。
**共享字符串常量池:**Java虚拟机(JVM)对字符串做了特殊处理,相同的字符串字面量会在内存中只有一份拷贝,这就是字符串常量池。由于String对象不可变,才能安全地让多个引用指向同一个字符串实例。
通过以上几点设计,String类成功实现了不可变性,保证了字符串一旦创建就不能被修改,无论在何种环境下,引用到一个String对象总是能看到其最初创建时的内容。这不仅确保了线程安全,还为Java中众多依赖于字符串不可变性的功能和算法提供了坚实的基础。

String为什么需要设计成不可变性

String类在Java中被设计为不可变性主要有以下几个重要原因:
**线程安全:**由于String对象一旦创建就不可更改,因此在多线程环境中,多个线程可以共享同一个String实例而不需要额外的同步控制,消除了线程间的数据竞争,增强了系统的并发性和安全性。
**内存效率:**不可变性使得字符串池(String Pool)成为可能,相同的字符串字面量在内存中只有一份拷贝,节约了内存空间。同时,由于字符串不变,哈希码可以被缓存起来,避免了每次计算的开销。
**安全性:**不可变字符串可以用作不可变标识符、类加载器的包名称和类名称等场景,确保了标识符在整个程序生命周期内的唯一性和一致性。
**集合类的性能优化:**在诸如HashMap、HashSet等集合类中,如果键是不可变的,那么它们的哈希值可以预先计算并且不会随时间变化,从而提高了哈希表操作的效率。
**缓存友好:**不可变对象更容易被缓存和重用,对于频繁使用和长时间存活的对象,可以显著提升性能。
**类库和框架依赖:**许多Java类库和框架(如JDBC、RMI等)假设字符串参数在整个方法调用期间不会发生变化,如果字符串可变,会导致难以预见的bug和安全漏洞。
综上所述,String设计为不可变性不仅简化了编程模型,也提供了很多性能和安全上的优势,是Java语言设计中的一大亮点。

为什么还需要StringBuffer、StringBuilder

在某些特定场景下,尤其是涉及到大量字符串操作和构建时,String类的不可变性可能会导致性能和内存开销的增加。这是因为每次对String对象进行修改操作时(如拼接、替换等),都需要创建新的String对象,而不是在原对象上进行修改。
为了克服这个问题,Java设计了StringBuffer和StringBuilder类,它们的主要目的是为了支持高效的字符串修改和构建操作,特别是对于大量字符串拼接而言:
可变性:StringBuffer和StringBuilder类的实例可以被修改,它们内部维护一个可变的字符数组,支持append、insert等方法,可以在原对象的基础上进行字符串操作,避免了创建大量临时字符串对象的过程。
线程安全:StringBuffer类在设计上采用了synchronized关键字对修改方法进行了同步处理,使其在多线程环境下安全地进行并发修改操作。而StringBuilder类没有进行线程安全处理,更适合单线程环境下的高性能字符串操作。
性能优化:在单线程环境下,StringBuilder由于没有线程安全的开销,其性能通常优于StringBuffer。在字符串频繁拼接或构造的场景下,使用StringBuilder能够显著减少内存分配次数和CPU开销。

其它

关注公众号【 java程序猿技术】获取八股文系列文章

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

后端马农

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

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

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

打赏作者

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

抵扣说明:

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

余额充值