• Java基础面试题


    目录

    1.== 和 equals 的区别是什么?

    2.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

    3.final在 java 中有什么作用?

    4.String 属于基础的数据类型吗?

    5.java 中操作字符串都有哪些类?它们之间有什么区别?

    6.如何将字符串反转?

    7.String 类的常用方法都有那些?

    8.抽象类必须要有抽象方法吗?

    9.普通类和抽象类有哪些区别?

    10.抽象类能使用 final 修饰吗?

    11.接口和抽象类有什么区别?

    12.重写和重载的区别?

    13.什么是多态?

    14.List、Set、Map 之间的区别是什么?

    15.HashMap 和 Hashtable 有什么区别?

    16. HashMap 的实现原理?

    17.ArrayList 和 LinkedList 的区别是什么?

    18.如何实现数组和 List 之间的转换?

    19.线程和进程的区别?

    20.创建线程有哪几种方式?

    21.runnable 和 callable 有什么区别?

    22.线程有哪些状态?

    23.sleep() 和 wait() 有什么区别?

    24.notify()和 notifyAll()有什么区别?

    25.线程的 run()和 start()有什么区别?


    1.== 和 equals 的区别是什么?

    1. == 是一个运算符,用于比较基本数据类型(如 int、char、boolean 等)时,它比较的是值是否相等;当用于比较引用类型(如对象、数组等)时,它比较的是两个对象的内存地址是否相同,即判断这两个对象是否是同一个对象实例。

    2. equals 是一个方法,定义在 java.lang.Object 类中。默认情况下,Object 类中的 equals 方法与 == 的行为一致,也是比较对象的引用是否相同。但是,很多类(如 StringInteger 等)重写了 equals 方法,使其比较的是对象的内容是否相等,而不是引用是否相同。这意味着,对于这些重写了 equals 方法的类,使用 equals 比较的是两个对象的逻辑上的等价性,而不是它们是否指向内存中的同一个位置。

    总结一下:

    • 当比较基本数据类型时,直接使用 == 比较值。
    • 当比较对象时,如果关心的是两个对象是否是同一个实例(地址相同),使用 ==
    • 如果关心的是两个对象的内容是否相等(逻辑上是否等价),应该使用 equals 方法,并注意该方法可能已被重写,需要根据具体类的行为来理解其比较逻辑。在使用自定义类时,通常建议根据业务需求重写 equals 以及 hashCode 方法,以实现内容的比较和保持两者逻辑上的一致性。

    2.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

    不对。两个对象的 hashCode() 相同,并不意味着它们的 equals() 方法一定会返回 truehashCode()equals() 遵循如下约定:

    1. 如果两个对象通过 equals() 方法比较结果为 true,那么这两个对象的 hashCode() 必须返回相同的值。
    2. 反之,如果两个对象的 hashCode() 返回相同的值,这并不意味着这两个对象通过 equals() 比较的结果也为 true。这是因为 hashCode() 方法的返回值范围有限(在 Java 中是一个整数),不同的对象可能会产生相同的哈希码,这称为哈希冲突。

    因此,hashCode() 主要用于快速查找、分组等场景,而 equals() 用于精确判断两个对象是否相等。设计良好的 hashCode() 方法应尽可能地减少冲突,提高基于哈希表的数据结构(如 HashMap、HashSet)的性能,但它不能用来替代 equals() 方法的功能。

    3.final在 java 中有什么作用?

    在Java中,`final` 关键字可以应用于类、方法和变量,具有以下不同作用:

    1. **修饰类(final class)**:
       - 当一个类被声明为 `final` 时,表示这个类不能被继承。这可以用来防止其他类继承并修改此类的行为,有助于确保类的设计不会被意外改变,增强系统的安全性和稳定性。例如,Java 标准库中的 `String` 类就是 final 的。

    2. **修饰方法(final method)**:
       - 如果一个方法被声明为 `final`,则子类不能覆盖(override)这个方法。这意味着该方法的行为在所有子类中都是固定的,确保了方法实现的统一性。这对于确保某些关键操作或算法不被子类修改非常重要。

    3. **修饰变量**:
       - `final` 修饰的变量被称为常量。根据变量的类型和位置,其含义略有不同:
         - **final 局部变量**:必须在声明时或构造方法/块中初始化一次,之后不能再被赋值。
         - **final 成员变量**(实例变量或静态变量):可以在声明时初始化,或者在构造方法中(对于实例变量)或静态初始化块中(对于静态变量)初始化。一旦初始化后,其值就不能再改变。
         - 对于基本类型的 `final` 变量,其值不能改变;对于引用类型的 `final` 变量,虽然引用本身不能改变(即不能指向另一个对象),但所指向的对象的内容是可以修改的(如果对象是可变的,如数组或集合)。

    使用 `final` 关键字可以帮助程序员表达代码的意图,增加代码的安全性和可读性,同时也有助于编译器进行优化。

    4.String 属于基础的数据类型吗?

    不属于,是final修饰的Java类。

    java中的基本数据类型:byte、char、short、int、long、float、double、boolean

    5.java 中操作字符串都有哪些类?它们之间有什么区别?

    在Java中,操作字符串主要涉及到以下三个类:StringStringBuilderStringBuffer。它们各自的特点和区别如下:

    1. String:

      • 不变性String 类表示不可变的字符序列,一旦创建,其内容就不能被修改。对字符串的任何修改操作(如拼接、替换等)都会返回一个新的 String 对象,而原始对象保持不变。
      • 效率:由于每次修改都会生成新的对象,频繁的修改操作会导致大量临时对象的产生,可能影响性能和内存使用。
      • 共享:由于字符串常量池的存在,相同的字符串字面量在内存中只有一份拷贝,这有利于节省内存。
    2. StringBuilder:

      • 可变性:提供可变的字符序列,允许对字符串进行修改,如添加、删除、替换字符等操作,而不会创建新的对象。
      • 线程非安全:它没有进行线程同步,因此在单线程环境下性能较好,是进行字符串拼接等操作的首选。
      • 性能:相比 String,在进行字符串修改操作时性能更优,因为它直接在对象的缓冲区上进行修改。
    3. StringBuffer:

      • 可变性:与 StringBuilder 类似,提供可变的字符序列,支持修改操作而不创建新对象。
      • 线程安全:与 StringBuilder 的主要区别在于它是线程安全的,即它的方法大多通过 synchronized 关键字进行了同步处理,适合在多线程环境中使用。
      • 性能:由于引入了同步机制,所以在单线程环境下,其性能通常不如 StringBuilder,但在多线程环境下能确保数据的一致性。

    总结

    • 对于不需要修改的字符串或在字符串操作较少的情况下,推荐使用 String 类,因其不可变性可以带来更好的安全性。
    • 在单线程环境下进行大量的字符串修改操作时,使用 StringBuilder 可以获得更高的性能。
    • 在多线程环境下,为了保证线程安全,应选择使用 StringBuffer,尽管它的性能因同步开销可能会略低于 StringBuilder

    6.如何将字符串反转?

    使用StringBuffer 或 StringBuilder 的 reverse 成员方法。

    7.String 类的常用方法都有那些?

    1. 获取信息的方法:

      • int length():返回字符串的长度。
      • char charAt(int index):返回指定索引位置的字符。
      • int indexOf(String str):返回指定子字符串第一次出现的索引,未找到返回-1。
      • int lastIndexOf(String str):返回指定子字符串最后一次出现的索引,未找到返回-1。
      • boolean isEmpty():判断字符串是否为空。
    2. 转换方法:

      • byte[] getBytes():将字符串转换成字节数组。
      • CharSequence subSequence(int beginIndex, int endIndex):返回字符串的一个子序列,即字符串的一部分。
      • String toLowerCase():将字符串转换成小写。
      • String toUpperCase():将字符串转换成大写。
    3. 比较方法:

      • boolean equals(Object anObject):比较字符串内容是否相等。
      • boolean equalsIgnoreCase(String anotherString):忽略大小写比较字符串内容是否相等。
      • int compareTo(String anotherString):按字典顺序比较两个字符串。
    4. 查找与替换:

      • String replace(char oldChar, char newChar):将字符串中所有指定字符替换为另一个字符。
      • String replaceAll(String regex, String replacement):使用正则表达式替换匹配到的内容。
      • String replaceFirst(String regex, String replacement):仅替换第一个匹配到的内容。
    5. 分割与连接:

      • String[] split(String regex):根据给定正则表达式拆分此字符串。
      • String concat(String str):将指定字符串连接到此字符串的末尾。
    6. 去除空白:

      • String trim():返回字符串的副本,忽略前导和尾随空白。
      • String strip():Java 11 引入,类似 trim() 但遵循Unicode空白规则。
    7. 判断方法:

      • boolean startsWith(String prefix):判断字符串是否以指定的前缀开始。
      • boolean endsWith(String suffix):判断字符串是否以指定的后缀结束。
      • boolean contains(CharSequence s):判断字符串是否包含指定的字符序列。
    8. 子串操作:

      • String substring(int beginIndex):返回从指定索引开始到字符串末尾的子串。
      • String substring(int beginIndex, int endIndex):返回指定索引范围内的子串。

    这只是 String 类的一部分常用方法,实际上还有更多,例如格式化、编码解码、与基本类型相互转换等方法。使用时可以根据具体需求查阅Java API文档获取详细信息。

    8.抽象类必须要有抽象方法吗?

    不是。抽象类可以没有抽象方法,但是如果你的一个类已经声明成了抽象类,即使这个类中没有抽象方法,它也不能再实例化,即不能直接构造一个该类的对象。如果一个类中有了一个抽象方法,那么这个类必须声明为抽象类,否则编译通不过。

    9.普通类和抽象类有哪些区别?

    普通类和抽象类在面向对象编程中具有以下几点主要区别:

    1. 实例化能力

      • 普通类:可以直接实例化,即可以通过new关键字创建其实例对象。
      • 抽象类:不能直接实例化,意味着不能直接创建抽象类的对象。抽象类的主要目的是作为其他类的基类使用。
    2. 抽象方法

      • 普通类:可以不包含任何抽象方法,即所有方法都可以有具体的实现。
      • 抽象类:可以包含抽象方法,抽象方法是没有具体实现的方法(方法体为空),由其子类来实现。包含抽象方法的类必须声明为抽象类。
    3. 继承与实现

      • 普通类:既可以被继承,也可以不继承其他类(除了Java中所有类都隐式继承自Object类)。
      • 抽象类:通常是为了被继承而设计,作为子类的模板,强制子类实现特定的抽象方法。
    4. 子类要求

      • 普通类的子类:没有特殊要求,可以自由选择实现或不实现父类的方法。
      • 抽象类的子类:必须实现抽象类中的所有抽象方法,除非子类本身也是抽象类。
    5. 构造函数

      • 两者都可以有构造函数,但抽象类的构造函数主要用于被子类调用以初始化父类成员,而不能直接用来创建抽象类的实例。
    6. 访问权限

      • 抽象类的抽象方法不能是private,因为子类需要访问这些方法来实现它们。抽象类的成员变量和方法虽然可以是protectedpublic,但通常不应为private,因为这样子类无法访问和重写。
    7. 设计目的

      • 普通类:关注于实现具体功能,提供完整的行为和属性定义。
      • 抽象类:更侧重于定义一组接口或模板,提供一个类族的通用行为框架,促进代码复用和设计的灵活性。

    综上所述,抽象类是面向对象设计中用于定义接口规范和提供部分实现的工具,而普通类则侧重于实现具体的业务逻辑和功能。

    10.抽象类能使用 final 修饰吗?

    不能

    11.接口和抽象类有什么区别?

    接口(Interface)和抽象类(Abstract Class)在Java中都是用于实现抽象化的概念,但它们之间存在一些关键区别:

    1. 抽象方法的实现

      • 接口:所有的方法默认都是抽象的(从Java 8开始,接口可以包含默认方法和静态方法,这些方法有具体实现)。接口不能包含实例字段,只能有静态常量(默认为public static final)。
      • 抽象类:可以包含抽象方法(没有具体实现的方法)和具体实现的方法。它可以有实例变量、构造方法以及静态方法。
    2. 多重继承

      • 接口:Java允许一个类实现多个接口,实现了多重继承的功能。
      • 抽象类:Java只允许一个类继承一个抽象类,遵循单一继承的原则。
    3. 访问修饰符

      • 接口:接口中的方法默认为public,且从Java 9开始可以使用private修饰方法(用于实现细节隐藏的私有方法)。接口中的字段总是public、static、final。
      • 抽象类:方法可以有任意访问修饰符(private、protected、public或默认),提供更大的灵活性。
    4. 设计理念

      • 接口:主要用来定义类型,是一种契约或协议,描述了类需要实现的行为,强调的是“能做什么”(what to do)。
      • 抽象类:不仅定义类型,还可以提供部分实现,通常用于表示类族间的共性,强调的是“是什么”(what it is)和“部分怎么做”(partially how to do it)。
    5. 初始化块与构造方法

      • 接口:不能有构造方法或初始化块。
      • 抽象类:可以有构造方法(尽管不能实例化抽象类,但构造方法可用于子类的初始化)和初始化块。
    6. 使用场景

      • 接口:当你需要定义多个类之间的共同行为,且这些行为不存在共同的状态时,使用接口更为合适。
      • 抽象类:如果你需要提供一些实现代码,同时定义一些抽象方法让子类去实现,或者需要有共同的状态时,使用抽象类会更加合适。

    总的来说,接口提供了完全的抽象,关注于行为的规范;而抽象类提供了部分实现,可以包含状态和行为,更适用于建立类的继承结构。在实际开发中,根据设计的具体需求选择使用接口或抽象类。

    12.重写和重载的区别?

    重写(Override)和重载(Overload)是面向对象编程中两个重要的概念,它们在Java等语言中有着明确的区分:

    重写(Override)

    1. 定义:重写发生在继承关系中,指的是子类对父类中已有的方法的实现进行替换。方法名、参数列表必须与父类中的方法完全相同,返回类型也必须兼容(或者是相同的,或者是父类返回类型的子类型)。
    2. 访问权限:子类中的重写方法的访问权限不能低于父类中被重写的方法的访问权限。
    3. 异常处理:子类方法声明的异常类型不能比父类方法更宽泛,可以抛出更窄的异常或者不抛出异常。
    4. 多态性:重写是运行时多态性的体现,即在运行时根据对象的实际类型决定调用哪个方法。

    重载(Overload)

    1. 定义:重载发生在同一个类内,指允许存在多个同名方法,这些方法的参数列表不同(参数的数量、类型或顺序不同),或者返回类型不同(但这不是主要区分依据,因为Java中重载不考虑返回类型)。
    2. 无关继承:重载与继承无关,它是在一个类内部实现的,用于提供执行相似任务但处理不同输入的能力。
    3. 访问权限、返回类型和异常:重载方法之间可以有不同的访问修饰符、返回类型和抛出的异常,这些不影响方法的重载。
    4. 编译时行为:重载是编译时多态性的体现,即在编译阶段根据传入参数的不同确定调用哪个方法。

    总结来说,重写是子类对父类方法的实现进行替换,要求方法签名完全一致(方法名和参数列表),主要用于实现多态;而重载则是在一个类内部提供多个同名但参数列表不同的方法,增加了程序的灵活性,是编译时的决策过程。

    13.什么是多态?

    一种事物的多种表现形态就是多态,比如定义一个类为动物,那么动物可以被子类继承,从而实现具体动物的方法

    14.List、Set、Map 之间的区别是什么?

    ListSetMap 是 Java 集合框架中的三个主要接口,它们各自有不同的特性和用途:

    1. List(列表)
      • 特性:List 是一个有序的集合,允许元素重复,即同一个元素可以在集合中出现多次。List 里的元素按照插入的顺序排列,同时也支持通过索引(整数)来访问元素,就像操作数组一样。
      • 例子:常见的实现有 ArrayList(基于动态数组,随机访问快,增删较慢)、LinkedList(基于双向链表,增删快,随机访问慢)。
    2. Set(集)
      • 特性:Set 是一个不允许元素重复的集合,即每个元素在集合中最多只能出现一次。Set 不保证元素的顺序,尽管某些实现(如 LinkedHashSet)可以保持插入顺序。元素在 Set 中的位置不由用户控制,而是由集合的内部实现决定。
      • 例子:常见的实现有 HashSet(基于哈希表,无序,插入和查询速度快)、TreeSet(基于红黑树,有序,可以自然排序或自定义排序)。
    3. Map(映射)
      • 特性:Map 不属于集合(Collection),而是一个关联容器,它存储键值对(key-value pair)。每个键(key)在 Map 中都是唯一的,但一个值(value)可以对应多个键。Map 本身不保证有序,但其某些实现(如 LinkedHashMapTreeMap)可以保持键的插入顺序或按键排序。
      • 例子:常见的实现有 HashMap(基于哈希表,无序,插入和查询速度快)、TreeMap(基于红黑树,按键排序)、LinkedHashMap(保持插入顺序)。

    总结

    • 数据结构:List 是线性结构,Set 也是集合结构但强调唯一性,Map 是键值对结构。
    • 元素重复性:List 允许重复,Set 不允许重复,Map 的键(key)是唯一的,但值(value)可以重复。
    • 遍历与访问:List 可通过索引访问,Set 和 Map 通常通过迭代器或键来访问。
    • 应用场景:根据是否需要元素有序、是否允许重复、是否需要键值对应关系来选择合适的数据结构。

    15.HashMap 和 Hashtable 有什么区别?

    HashMapHashtable 都是 Java 中用来存储键值对(key-value pairs)的数据结构,但它们之间存在一些关键差异:

    1. 线程安全性

      • Hashtable:它是线程安全的,意味着在多线程环境下无需外部同步即可安全使用。为了确保线程安全,Hashtable 的每个方法都经过了同步处理(使用 synchronized 关键字),这虽然增加了安全性,但可能导致性能降低。
      • HashMap:它是非线程安全的,更适合在单线程环境或已同步的多线程环境中使用。由于没有同步开销,HashMap 在单线程环境下的性能通常优于 Hashtable
    2. 对 null 键值的支持

      • HashMap:允许一个 null 键和多个 null 值。由于键的唯一性,只能有一个键为 null,但多个值可以是 null
      • Hashtable:既不允许 null 键也不允许 null 值。尝试插入 null 将会抛出 NullPointerException
    3. 继承关系和接口实现

      • HashMap 继承自 AbstractMap 类,并直接实现了 Map 接口。
      • Hashtable 继承自更古老的 Dictionary 类,同样实现了 Map 接口,同时还实现了 Cloneable 和 Serializable 接口。
    4. 性能

      • 由于 HashMap 不需要进行线程同步,所以在没有线程安全需求时,它的执行效率通常比 Hashtable 更高。
    5. 计算哈希值的方法

      • 两者计算哈希值的方法有所不同,尽管这些内部细节对于大多数用户来说通常是透明的。
    6. 初始容量和扩容机制

      • 它们在初始容量和扩容因子方面有所不同,尽管这些参数都可以在创建时通过构造函数进行设置。
    7. 迭代器的行为

      • HashMap 的迭代器是 fail-fast 的,意味着在迭代过程中如果集合被修改(除了通过迭代器自己的 remove 方法),迭代器会迅速失败,抛出 ConcurrentModificationException
      • 虽然 Hashtable 的迭代器在早期实现中没有明确说明是否 fail-fast,但在现代 Java 版本中,其迭代器也应该是 fail-fast 的,以保持与 HashMap 等其他集合类的兼容性。

    总的来说,HashMap 通常在单线程或已同步的环境中被推荐使用,因为它提供了更好的性能,而 Hashtable 则适用于那些不需要关心性能而需要绝对线程安全的场景。随着 ConcurrentHashMap 的引入,后者在许多多线程场景下已经替代了 Hashtable,因为它提供了更好的并发性能。

    16. HashMap 的实现原理?

    HashMap 是 Java 集合框架中的一个实现,它提供了一种键值对(key-value)的数据结构,主要用于快速的键查找。HashMap 的实现原理主要基于以下几个核心点:

    1. 哈希表结构:HashMap 的核心是一个数组(称为“桶”或“槽”),每个桶可以存放一个或多个键值对(在内部表现为 Node 或者 TreeNode 结构)。这个数组的大小通常是2的幂次方,以便于计算桶的位置。

    2. 哈希函数:当一个键值对被加入 HashMap 时,首先通过键的 hashCode() 方法计算出一个哈希值,然后通过特定的算法(通常是 (n - 1) & hash,其中 n 是桶的数量)将哈希值转换为桶的索引。这个过程确保了哈希值到桶索引的映射尽可能均匀分布。

    3. 解决哈希冲突:由于不同的键可能得到相同的哈希值,从而导致冲突,HashMap 通过两种方式解决这个问题:

      • 链地址法(JDK 1.8 及之前版本的主要方式):每个桶实际上是一个链表,当有冲突时,新来的键值对会被添加到该桶对应的链表中。
      • 红黑树(从 JDK 1.8 引入):当桶中的链表长度超过某个阈值(默认是8)且 HashMap 的容量足够大时,该链表会被转换为红黑树,以提高查找效率,从 O(n) 提升到 O(log n)。
    4. 负载因子(Load Factor):这是衡量 HashMap 多满的一个指标,定义为填满的桶的数量除以桶的总数。当负载因子达到某个阈值(默认是0.75)时,HashMap 会自动进行扩容,通常是将桶的数量翻倍,并重新分配所有键值对,这个过程会重新计算它们在新数组中的位置,以维持良好的性能。

    5. 线程安全性:HashMap 是非线程安全的。如果在多线程环境下需要安全地使用 HashMap,可以考虑使用 ConcurrentHashMap,它通过分段锁等机制实现了线程安全且高效的并发访问。

    6. 插入与删除:插入操作会计算键的哈希值并找到对应的桶,然后根据情况插入到链表末尾或树的适当位置。删除操作则需要找到键对应的节点并从链表或树中移除。

    综上所述,HashMap 通过哈希函数快速定位桶的位置,使用链表或红黑树处理冲突,以及适时的扩容机制来维持高效的数据存取。

    17.ArrayList 和 LinkedList 的区别是什么?

    ArrayListLinkedList 都是 Java 集合框架中的实现,它们都实现了 List 接口,因此可以提供列表类型的功能,如按索引位置进行元素的插入、删除和访问。但它们在内部数据结构、性能特点和适用场景上存在显著差异:

    数据结构

    • ArrayList:基于动态数组实现。这意味着它在内部维护了一个可以自动调整大小的数组。数组的特点是连续的内存空间,支持随机访问,即通过索引快速获取元素。

    • LinkedList:基于双向链表实现。链表中的每个元素(节点)都包含数据和两个指针,分别指向下一个和前一个节点。这种结构不保证元素在内存中是连续存储的,因此不适合随机访问,但插入和删除操作更快,尤其是当操作发生在链表两端时。

    性能特点

    • 随机访问(get, set)ArrayList 由于其数组结构,支持快速的随机访问,时间复杂度为 O(1)。而 LinkedList 需要从头或尾开始遍历,直到找到指定索引的元素,时间复杂度为 O(n)。

    • 插入和删除

      • 在列表的中间进行插入或删除操作时,LinkedList 的性能更好,因为仅需调整相邻节点的指针,时间复杂度为 O(1)(假设已经找到了操作的位置)。
      • 对于 ArrayList,插入或删除元素会导致后续元素需要移动来保持数组的连续性,最坏情况下时间复杂度为 O(n)。

    内存使用与缓存友好性

    • ArrayList 因为需要连续内存,可能在内存分配和管理上更为高效,特别是在元素数量较大且相对稳定时。
    • LinkedList 每个节点除了存储数据外,还需要额外的内存来保存前后节点的引用,因此在存储相同数量的元素时,总体内存占用通常比 ArrayList 高。
    • 由于 ArrayList 的连续性,它更符合 CPU 缓存的工作原理(局部性原理),在进行连续元素访问时,CPU 缓存可以更有效地发挥作用,提高访问速度。

    适用场景

    • ArrayList 适用于需要频繁进行随机访问,且插入删除操作较少的场景。
    • LinkedList 适用于频繁进行插入和删除操作,特别是操作发生在列表两端的场景,或者需要作为队列、栈使用时。

    综上所述,选择 ArrayList 还是 LinkedList 主要取决于具体的应用需求,包括对数据访问模式、性能要求和内存使用的考量。

    18.如何实现数组和 List 之间的转换?

    List转数组:String[] list = List.toArray(array);//array为List

    数组转List:List list = java.util.Arrays.asList(array);//array为数组

    19.线程和进程的区别?

    进程是资源分配最小单位,线程是程序执行最小单位

    每个进程都有相应的线程

    进程有独立的地址空间,线程没有

    线程是指处理机调度的基本单位

    进程执行开销大,线程执行开销小

    20.创建线程有哪几种方式?

    继承Thread类并实现run方法,调用继承类的start方法开启线程;

    通过实现Runnable接口,重写run方法,调用线程对象的start方法开启线程;

    除此之外,还可以通过实现Callable接口,实现call方法,并用FutureTask类包装Callable对象开启线程。

    21.runnable 和 callable 有什么区别?

    RunnableCallable 都是 Java 中用于实现多线程任务的接口,但它们之间存在几个关键区别:

    1. 返回值

      • Runnable 接口没有返回值,其 run() 方法是一个 void 方法,只执行任务而不产生结果。
      • Callable 接口有一个返回值,通过 call() 方法可以返回一个泛型类型的结果,这使得它能够提供任务执行后的输出。
    2. 异常处理

      • Runnable 的 run() 方法不能直接抛出受检异常(checked exceptions),任何需要处理的异常都必须在方法内部捕获并处理。
      • Callable 的 call() 方法允许抛出受检异常,这意味着它可以抛出任何类型的异常,并且调用者需要处理这些异常,提供了更灵活的错误处理机制。
    3. 使用方式

      • 实现 Runnable 的任务可以直接传递给 Thread 构造函数,或者提交给 ExecutorService 的 execute() 方法来执行。
      • 实现 Callable 的任务通常通过 ExecutorService 的 submit() 方法提交,这会返回一个 Future 对象,可以用它来获取任务的返回结果或检查任务状态。Future 提供了对异步计算结果的访问方法。
    4. 功能和设计目的

      • Runnable 更适用于那些不需要返回结果,也不需要抛出受检异常的简单并发任务。
      • Callable 则适用于复杂的、需要有返回结果或可能抛出异常的多线程任务,提供了更强大和灵活的任务执行模型。

    综上所述,选择 Runnable 还是 Callable 取决于你的任务是否需要返回结果以及是否需要处理受检异常。对于需要高级特性的并发任务,Callable 和配合它的 Future 通常提供更好的解决方案。

    22.线程有哪些状态?

    新建、就绪、运行、阻塞、死亡

    23.sleep() 和 wait() 有什么区别?

    sleep()wait() 都可以暂停线程的执行,但它们之间存在几个重要的区别:

    1. 所属类不同

      • sleep() 是 Thread 类的静态方法,可以直接通过 Thread.sleep(long millis) 调用,使当前正在执行的线程暂停执行指定的毫秒数。
      • wait() 是 Object 类的一个实例方法,必须在同步代码块或同步方法中被调用,即当前线程必须拥有某个对象的监视器锁(monitor)。
    2. 是否释放锁

      • 当线程调用 sleep() 方法时,它不会释放任何锁。如果有同步锁,线程仍然保持这些锁。
      • 而当线程调用 wait() 方法时,它会释放它所持有的对象的锁,从而使其他正在等待该对象锁的线程有机会执行。
    3. 唤醒方式

      • sleep() 方法会根据指定的时间自动唤醒线程,无需外部干预。
      • wait() 方法则需要其他线程调用同一对象上的 notify() 或 notifyAll() 方法来唤醒。如果没有指定等待时间,它会一直等待,直到被通知。
    4. 异常处理

      • 两者都会抛出 InterruptedException,表示线程在等待过程中被中断。
    5. 使用目的

      • sleep() 主要用于让当前线程暂停执行一段时间,目的是为了给其他线程执行的机会,或者作为简单的延迟手段。
      • wait() 用于线程间的同步与通信,让线程等待某个条件满足,常用于生产者-消费者模式等场景。

    总结来说,sleep() 更多用于线程自身的调度和简单延时,而 wait() 则是用于线程间的协调与通信,涉及到复杂的线程间同步问题。

    24.notify()和 notifyAll()有什么区别?

    notify()方法会唤醒对象等待池中的一个线程,进入锁池;

    notifyAll()方法会唤醒等待池中的所有线程,进入锁池

    25.线程的 run()和 start()有什么区别?

    在Java中,线程的 run() 方法和 start() 方法有本质的区别:

    • run() 方法:

      • run() 方法包含了线程执行的具体逻辑代码。当你重写 Thread 类或实现 Runnable 接口时,你需要提供自己的 run() 方法实现。
      • 直接调用 run() 方法就如同调用任何其他普通方法一样,它会在当前线程中顺序执行,而不会创建新的线程。这意味着如果在主线程中直接调用某个线程对象的 run() 方法,那么该方法中的代码会立即执行,并且是单线程执行,不会实现并发效果。
    • start() 方法:

      • start() 方法是真正启动线程的方法。当你调用一个线程对象的 start() 方法时,Java虚拟机(JVM)会为该线程分配必要的系统资源,并将线程设置为可运行状态(Runnable)。随后,JVM会在适当的时候(具体由其线程调度策略决定)安排新线程获得CPU时间片并开始执行。
      • 在新线程启动后,JVM会自动调用该线程的 run() 方法。因此,通过 start() 方法不仅启动了一个新线程,还确保了 run() 方法在该新线程的上下文中执行,从而实现了多线程并发执行。

    简而言之,如果你想让一段代码以多线程的方式并发执行,你应该调用线程对象的 start() 方法,而不是直接调用 run() 方法。直接调用 run() 方法不会创建新线程,也不会实现并发执行。

  • 相关阅读:
    如何使用javascript制作一个网页端3D贪吃蛇游戏(附源码及链接)
    OpenHarmony 社区运营报告(2023 年 10 月)
    雷顿学院大数据(一期课程)
    Dubbo 我手写几行代码,就把通信模式给你解释清楚!
    Redis介绍
    手撕 LFU 缓存
    力扣 095. 最长公共子序列(C语言+动态规划)
    笔试强训48天——day21
    将轨迹显示到卫星地图上
    探讨Socks5代理技术的原理及其在不同领域的应用
  • 原文地址:https://blog.csdn.net/kang_tian/article/details/139440115