单词反转和数组去重,附经典面试题一份
博彦科技笔试:
给定字符,拼接成单词进行反转单词;
package org.example;
public class Main {
public static void main(String[] args) {
char[] input = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', ' ', 'J', 'a', 'v', 'a'};
String inputToString = new String(input);
System.out.println("Input: " + inputToString);
String[] words = inputToString.split(" ");
String res = "";
for (int i= words.length-1; i>=0; i--){
res+=words[i];
if (i != 0){
res+=" ";
}
}
char[] output = res.toCharArray();
System.out.println("----------------------------------");
for(char c: output)
System.out.print(c);
}
}
起初只想到了用split进行分割,忘记了是String 类型才有的,结果是String 与char类型的数组转化,用到了.toCharArray();
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* @param s string字符串
* @param n int整型
* @return string字符串
*/
public String trans(String s, int n) {
// 用来存储最终的结果,初始化一个空的 StringBuilder 实例
StringBuilder res = new StringBuilder();
// 去掉字符串两端的空白字符,使用正则分割字符串,处理多个空格
String[] words = s.split(" ");
// 从后往前遍历单词数组
for (int i = words.length - 1; i >= 0; i--) {
// 将当前单词转换为字符数组
char[] word = words[i].toCharArray();
// 遍历当前单词的每个字符,反转大小写
for (char c : word) {
if (Character.isLowerCase(c)) {
res.append(Character.toUpperCase(c));
} else {
res.append(Character.toLowerCase(c));
}
}
// 如果不是最后一个单词,则在后面加一个空格
if (i != 0) {
res.append(" ");
}
}
// 返回最终的结果字符串
return res.toString();
}
}
没有AC
保留两位小数的格式
package org.example;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
Double d = scanner.nextDouble();
System.out.println(String.format("%.2f",d));
}
}
package org.example;
import java.text.DecimalFormat;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
Double d = scanner.nextDouble();
DecimalFormat df = new DecimalFormat("#.00");
System.out.println(df.format(d));
}
}
(北京)有限公司:
*1. package testpkg.p1;
public class ParentUtil
{
public int x = 420;
protected int doStuff() { return x; }
}
package testpkg.p2;
import testpkg.p1.ParentUtil;
public class ChildUtil extends ParentUtil
{
public static void main(String [] args)
{
new ChildUtil().callStuff();
}
void callStuff()
{
System.out.print("this " + this.doStuff() ); /* Line 18 */
ParentUtil p = new ParentUtil();
System.out.print(" parent " + p.doStuff() ); /* Line 20 */
}
}有错误吗
解释:
这个代码有两个包(testpkg.p1
和 testpkg.p2
)和继承关系。我们来逐步分析代码,看看是否有错误。
代码结构分析:
1. ParentUtil 类(位于 testpkg.p1
包)
package testpkg.p1;
public class ParentUtil {
public int x = 420;
protected int doStuff() {
return x;
}
}
ParentUtil
类定义了一个公开字段x
和一个受保护的doStuff
方法。doStuff
方法是protected
的,意味着它只能在同一个包内或者子类中访问。- 没有错误,代码正确。
2. ChildUtil 类(位于 testpkg.p2
包)
package testpkg.p2;
import testpkg.p1.ParentUtil;
public class ChildUtil extends ParentUtil {
public static void main(String[] args) {
new ChildUtil().callStuff();
}
void callStuff() {
System.out.print("this " + this.doStuff()); /* Line 18 */
ParentUtil p = new ParentUtil();
System.out.print(" parent " + p.doStuff()); /* Line 20 */
}
}
ChildUtil
类继承自ParentUtil
。main
方法创建了ChildUtil
实例并调用了callStuff
方法。callStuff
方法中,首先打印了this.doStuff()
,然后创建了一个ParentUtil
对象并打印了p.doStuff()
。
错误分析:
1. Line 18 (this.doStuff()
调用)
this.doStuff()
调用是合法的,因为doStuff
是protected
的,而ChildUtil
是ParentUtil
的子类。protected
方法可以在子类中访问,所以this.doStuff()
是合法的,并且能够访问父类的doStuff
方法。
2. Line 20 (p.doStuff()
调用)
- 在这一行,创建了一个
ParentUtil
类型的对象p
,然后尝试调用p.doStuff()
。 - 问题出在这里:
doStuff
方法是protected
的,它只能在同一个包内或子类中被访问。 p
是ParentUtil
类型的对象,且ParentUtil
和ChildUtil
处于不同的包(分别在testpkg.p1
和testpkg.p2
中)。因此,在testpkg.p2
中直接访问p.doStuff()
是不合法的。
错误的原因:
- 由于
doStuff
是protected
,它无法在不同包的非子类中被访问,因此p.doStuff()
会导致编译错误。
解决方案:
有两种可能的解决方法来解决 Line 20
的错误。
-
更改
doStuff
方法的访问修饰符:
如果希望在testpkg.p2
包中访问doStuff
方法,可以将doStuff
方法的访问修饰符更改为public
,这样它就可以被其他包的类访问。public int doStuff() { return x; }
-
通过继承调用
doStuff
:
如果你不希望修改doStuff
的访问级别,可以通过子类对象来调用doStuff
方法。例如,将p
的类型改为ChildUtil
,这样就能通过子类访问受保护的方法。ParentUtil p = new ChildUtil(); System.out.print(" parent " + p.doStuff());
这种方法可以确保
doStuff
在子类中被访问,从而不会导致权限错误。
结论:
Line 18
没有错误,因为在子类中可以访问protected
方法。Line 20
存在错误,因为doStuff
是protected
的,ParentUtil
和ChildUtil
在不同的包中,导致p.doStuff()
无法访问。
通过上述两种解决方案中的任何一种,你可以修复代码中的错误。
- *2. 哪个集合类允许扩大或缩小其大小,并提供对其元素的索引访问,但其方法不是同步的(synchronized)?
A.java.util.HashSet
B.java.util.LinkedHashSet
C.java.util.List
D.java.util.ArrayList
答案是 D. java.util.ArrayList
。
解释:
让我们逐一分析每个选项:
A. java.util.HashSet
HashSet
是一个集合类,它存储不重复的元素,并且没有提供按索引访问元素的功能。它是一个 无序集合,因此不能按索引访问元素。- 此外,
HashSet
并没有提供同步机制,它的所有方法不是同步的。
B. java.util.LinkedHashSet
LinkedHashSet
是一个继承自HashSet
的集合类,它也存储不重复的元素,并且保持元素插入的顺序。与HashSet
一样,它不提供按索引访问元素的功能。- 同样,
LinkedHashSet
也没有同步机制,方法也不是同步的。
C. java.util.List
List
是一个接口,它定义了一个有序的集合,并允许通过索引访问元素。它的子接口有ArrayList
和LinkedList
等。虽然List
确实提供索引访问,但这个选项本身是一个接口,不是一个具体的实现类,因此不适合作为答案。
D. java.util.ArrayList
ArrayList
是一个实现了List
接口的集合类,它提供了可变大小的数组,并允许通过索引访问元素。ArrayList
的大小是可以自动扩展和缩小的。它在添加或删除元素时,会动态调整其内部数组的大小。ArrayList
的方法 不是同步的,这意味着它不是线程安全的,多个线程同时修改同一个ArrayList
时需要额外的同步机制来保证线程安全。
结论:
ArrayList
是唯一符合题目描述的集合类,它允许扩展或缩小大小,提供索引访问,并且其方法不是同步的。
所以,正确答案是 D. java.util.ArrayList
。
3、哪种方法不会直接导致线程停止?
A.notify()
B.wait()
C. InputStream access
D. sleep()
正确答案是 A. notify()
。
解释:
让我们逐一分析每个选项:
A. notify()
-
notify()
方法是用于唤醒在同步代码块或方法中等待的线程,但它不会直接导致线程停止。它仅仅是唤醒等待的线程,并允许线程继续执行。线程在调用notify()
时并不会停止,它只是将线程的状态从等待中唤醒,使其有机会重新获取锁并继续执行。 -
总结:
notify()
不会直接导致线程停止,它的作用是唤醒其他线程。
B. wait()
-
wait()
方法是用于使当前线程进入等待状态,直到其他线程通过notify()
或notifyAll()
唤醒它。调用wait()
后,线程会被挂起并进入 等待队列,直到它被唤醒后才会继续执行。因此,wait()
会导致线程停顿,直到被唤醒。 -
总结:
wait()
会直接导致线程停顿,直到被唤醒。
C. InputStream access
-
InputStream
访问(例如read()
方法)会使当前线程在等待输入数据时阻塞,直到数据可用或发生错误。然而,阻塞并不意味着线程停止,它只是挂起当前线程,等待输入流中的数据。线程会在等待期间被阻塞,但它不会像wait()
那样主动进入 “等待” 状态;它只是等待数据的到来。 -
总结:
InputStream
的访问可能会阻塞线程,但它不会直接导致线程停止。
D. sleep()
-
sleep()
方法使当前线程暂停执行指定的时间。它不会释放锁,仅仅使当前线程进入 睡眠状态,在指定时间后自动恢复执行。虽然线程暂停,但线程并没有停止,它仍然存在,并会在sleep()
时间到期后继续执行。 -
总结:
sleep()
会使线程暂停一段时间,但不会直接停止线程,它会在时间结束后恢复执行。
结论:
notify()
只是唤醒等待的线程,并不会直接导致线程停止。其他方法(wait()
、sleep()
、InputStream access
)都可能导致线程停止或挂起。
因此,正确答案是 A. notify()
。
4、
class X implements Runnable
{
public static void main(String args[])
{
/* Missing code? */
}
public void run() {}
}
下列哪种方法可以启动线程?
A.Thread t = new Thread(X);
B. Thread t = new Thread(X); t.start();
C. X run = new X(); Thread t = new Thread(run); t.start();
D. Thread t = new Thread(); x.run();
正确答案是 C. X run = new X(); Thread t = new Thread(run); t.start();。
解释:
-
A. Thread t = new Thread(X);
- 这行代码是错误的。
X
是一个类,不是一个对象。要将Runnable
对象传递给Thread
构造函数,必须创建X
类的实例。所以,直接将X
类传递给Thread
构造函数会导致编译错误。
- 这行代码是错误的。
-
B. Thread t = new Thread(X); t.start();
- 这也是错误的,原因同上,
Thread
的构造函数需要一个Runnable
对象作为参数,而不能直接传递类名。应该传递Runnable
实例。
- 这也是错误的,原因同上,
-
C. X run = new X(); Thread t = new Thread(run); t.start();
- 这是正确的做法。首先,创建了一个
X
类的实例run
,它实现了Runnable
接口。然后,将该实例作为参数传递给Thread
构造函数,并调用start()
启动线程。start()
方法会调用run()
方法,开始线程的执行。
- 这是正确的做法。首先,创建了一个
-
D. Thread t = new Thread(); x.run();
- 这行代码是错误的。首先,
Thread
类的构造函数没有传递Runnable
实例。此外,直接调用x.run()
并不会启动新线程,而是会在当前线程中执行run()
方法,导致线程未真正启动。
- 这行代码是错误的。首先,
结论:
为了启动线程,正确的方式是先创建一个实现了 Runnable
接口的实例,然后将其传递给 Thread
类的构造函数,最后调用 start()
方法启动线程。
因此,正确答案是 C. X run = new X(); Thread t = new Thread(run); t.start();。
5、
*5. 哪三个保证一个线程会离开运行状态
yield()
wait()
notify()
notifyAll()
sleep(1000)
aLiveThread.join()
Thread.killThread()
要确保一个线程离开运行状态并进入其他状态(如 等待状态、阻塞状态、终止状态 等),我们可以使用以下方法:
1. yield()
- 作用:
yield()
是一个静态方法,表示当前线程主动让出 CPU 时间片,但不保证线程会立即停止运行。调用yield()
后,当前线程会被挂起,操作系统会重新调度其他线程。然而,线程的状态将变为 就绪状态,而不是 阻塞状态。 - 结论:
yield()
只会让当前线程 暂停执行,但不会离开运行状态,线程会在稍后的时间恢复执行。
2. wait()
- 作用:
wait()
会使当前线程进入 等待状态,直到它被其他线程唤醒(通常通过notify()
或notifyAll()
)。在等待期间,线程不再占用 CPU,因此它会离开运行状态。 - 结论:
wait()
确实会导致线程离开运行状态,进入 等待状态。
3. notify()
- 作用:
notify()
是唤醒一个在当前对象监视器上等待的线程,但不会直接影响当前线程的状态。它只是通知等待线程有条件去执行,并不会让当前线程离开运行状态。 - 结论:
notify()
不会导致当前线程离开运行状态,它只会唤醒等待线程。
4. notifyAll()
- 作用:
notifyAll()
是唤醒当前对象监视器上所有等待的线程,同样,它并不影响当前线程的状态。 - 结论:
notifyAll()
只会唤醒所有等待线程,并不会让当前线程离开运行状态。
5. sleep(1000)
- 作用:
sleep(1000)
使当前线程进入 阻塞状态,并且会在指定时间(此处为 1000 毫秒)后自动恢复执行。 - 结论:
sleep()
会使线程离开运行状态,进入 阻塞状态,直到睡眠时间结束。
6. aLiveThread.join()
- 作用:
join()
方法使当前线程等待aLiveThread
执行完毕。如果aLiveThread
还没有执行完,调用join()
的线程会进入 阻塞状态,直到aLiveThread
完成。 - 结论:
join()
会让当前线程离开运行状态,进入 阻塞状态,直到目标线程执行完成。
7. Thread.killThread()
- 作用:
Thread.killThread()
并不是 Java 中的有效方法。实际上,Java 中并没有提供直接杀死线程的 API。线程的停止应通过其他方式(如设置标志位或让线程自然终止)。 - 结论:没有
killThread()
方法,不适用。
总结:
根据线程离开运行状态的标准,我们可以得到以下结论:
wait()
:会导致线程进入等待状态。sleep(1000)
:会导致线程进入阻塞状态。aLiveThread.join()
:会导致当前线程进入阻塞状态,直到目标线程执行完毕。
正确答案:
wait()
sleep(1000)
aLiveThread.join()
这三种方法能够确保一个线程离开运行状态。
线程的几种状态分别是?
6. int i = l, j = -1;
switch (i)
{
case 0, 1: j = 1; /* Line 4 */
case 2: j = 2;
default: j = 0;
}
System.out.println("j = " + j);有错误吗,结果是
错误分析:
这段代码存在语法错误:
int i = l, j = -1;
switch (i) {
case 0, 1: j = 1; /* Line 4 */
case 2: j = 2;
default: j = 0;
}
System.out.println("j = " + j);
1. 语法错误:
- 问题 1:
switch
语句中,case
标签通常只能列出一个常量值。Java 14 引入了 增强的switch
语法(使用逗号分隔多个值),但只有从 Java 14 开始才支持case 0, 1:
的写法,前提是启用了 switch 表达式 或使用了增强版switch
。 - 问题 2:在该代码中,
l
是一个变量,而不是常量。如果l
没有定义,这将导致编译错误。i
需要是一个有效的整数常量值。
2. 修正:
- 修改 1:首先,确保
i
和j
的定义是正确的,并且i
的值是常量。假设i = 1
。 - 修改 2:如果使用传统的
switch
语法(即没有启用 Java 14+ 的增强switch
),每个case
必须单独指定一个常量。
3. 代码修复:
int i = 1, j = -1;
switch (i) {
case 0:
case 1:
j = 1;
break; // Adding break to prevent fall-through
case 2:
j = 2;
break;
default:
j = 0;
}
System.out.println("j = " + j);
4. 结果分析:
i = 1
时,switch
会检查case 1
,并进入该分支。case 1
执行后将j = 1
,然后执行break
语句,跳出switch
语句。- 因此,
j = 1
,最后System.out.println("j = " + j);
打印出j = 1
。
5. 增强 switch
语法(如果你使用 Java 14+):
如果你启用了增强 switch
语法,代码可以写成这样:
int i = 1, j = -1;
switch (i) {
case 0, 1:
j = 1;
break; // break is optional in switch expressions
case 2:
j = 2;
break;
default:
j = 0;
}
System.out.println("j = " + j);
在这种语法下,case 0, 1:
表示 i
是 0
或 1
,都将执行 j = 1
。
总结:
- 错误:原始代码中有一个语法错误,
case 0, 1:
是增强switch
语法的一部分,只有在 Java 14+ 启用该语法时才有效。 - 修复后的结果:修正后的代码中,假设
i = 1
,最终输出j = 1
。
7. public class SwitchTest
{
public static void main(String[] args)
{
System.out.println("value =" + switchIt(4));
}
public static int switchIt(int x)
{
int j = 1;
switch (x)
{
case l: j++;
case 2: j++;
case 3: j++;
case 4: j++;
case 5: j++;
default: j++;
}
return j + x;
}
}
错误分析:
这段代码有几个问题,首先我们逐一分析:
1. case l:
语法错误
- 在
switch
语句中,l
是一个变量,而switch
的case
标签必须是常量。因此,case l:
会导致编译错误。 - 如果
l
是一个数字常量(比如l = 1;
),则case
应该是常量值1
,而不是变量名l
。
2. 缺少 break
语句
- 由于没有
break
语句,每个case
会出现 “fall-through”(掉入下一个case
)的现象。这意味着,如果x
为某个case
,程序将继续执行该case
后面所有的代码,直到遇到break
(或者到达switch
末尾)。
3. 修正代码:
- 我们假设
l
是一个常量1
,并且修正其他潜在问题。
修正后的代码:
public class SwitchTest {
public static void main(String[] args) {
System.out.println("value = " + switchIt(4));
}
public static int switchIt(int x) {
int j = 1;
switch (x) {
case 1: // 修正了 l 为常量 1
j++; // 进入 case 1 后,j 会加 1
break; // 加入 break 防止继续 fall-through
case 2:
j++;
break;
case 3:
j++;
break;
case 4:
j++;
break;
case 5:
j++;
break;
default:
j++; // 默认情况下 j 也会增加
break;
}
return j + x;
}
}
4. 代码解释:
switchIt(4)
被调用时,x
的值是 4。switch
语句开始时,检查x
的值是否匹配各个case
标签。case 4:
匹配x = 4
,进入case 4
,j
的值变为 2。- 由于每个
case
后都有break
,程序会跳出switch
语句,不会继续执行case 5
或default
。 - 最终,
switchIt(4)
返回j + x
,即2 + 4 = 6
。
5. 输出:
value = 6
6. 总结:
- 问题:原始代码中的
case l:
语法错误,且没有break
,会导致fall-through
。 - 修正:将
case l:
改为常量case 1:
,并且为每个case
添加break
语句。 - 结果:修正后的代码输出
value = 6
。
关于java.util.HashSet下列说法正确的是?
A.集合中的元素是有序的。
B. 集合保证是不可变的。
C.集合中的元素保证是唯一的。
D.使用唯一键访问集合中的元素。
关于 java.util.HashSet
,正确的说法是 C。
逐项分析:
-
A. 集合中的元素是有序的。
- 错误:
HashSet
不保证元素的顺序。它是一个 无序 集合,意味着插入元素的顺序和遍历顺序可能不一致。如果你需要元素保持插入顺序,可以使用LinkedHashSet
。
- 错误:
-
B. 集合保证是不可变的。
- 错误:
HashSet
本身是可变的,可以添加和删除元素。如果你需要不可变的集合,可以使用Collections.unmodifiableSet()
或Set.of()
(在 Java 9 及以后版本)。
- 错误:
-
C. 集合中的元素保证是唯一的。
- 正确:
HashSet
保证集合中的元素是唯一的。如果你试图向HashSet
中添加重复的元素,它将不会被添加。重复元素的定义是根据元素的equals()
方法和hashCode()
方法来判定的。
- 正确:
-
D. 使用唯一键访问集合中的元素。
- 错误:
HashSet
并不像HashMap
那样使用键(key)来访问元素。HashSet
是基于集合的,没有 “键” 的概念,它只关心元素是否存在。HashMap
才是基于键值对的结构,提供基于键的访问。
- 错误:
正确答案:
C. 集合中的元素保证是唯一的。
9. public class RTExcept
{
public static void throwit ()
{
System.out.print("throwit ");
throw new RuntimeException();
}
public static void main(String [] args)
{
try
{
System.out.print("hello ");
throwit();
}
catch (Exception re )
{
System.out.print("caught ");
}
finally
{
System.out.print("finally ");
}
System.out.println("after ");
}
}结果是
让我们逐步分析这段代码,理解它的执行过程,并得出结果。
代码分析:
public class RTExcept {
public static void throwit() {
System.out.print("throwit ");
throw new RuntimeException(); // 抛出运行时异常
}
public static void main(String[] args) {
try {
System.out.print("hello ");
throwit(); // 调用 throwit() 方法,它会抛出异常
}
catch (Exception re) {
System.out.print("caught "); // 捕获异常并打印 "caught"
}
finally {
System.out.print("finally "); // 无论如何都会执行,打印 "finally"
}
System.out.println("after "); // 这行代码在 finally 执行后执行
}
}
执行流程:
-
进入
main
方法:- 首先打印
"hello "
。
- 首先打印
-
调用
throwit()
方法:throwit()
被调用,打印"throwit "
。- 然后,
throwit()
抛出一个RuntimeException
。
-
异常被
catch
捕获:RuntimeException
是Exception
类的子类,因此它被catch
块中的Exception
捕获。- 捕获异常后,打印
"caught "
。
-
finally
块执行:- 不管是否有异常,
finally
块都会执行,打印"finally "
。
- 不管是否有异常,
-
after
被打印:finally
块执行完后,程序继续执行System.out.println("after ");
,打印"after"
。
输出结果:
hello throwit caught finally after
解释:
"hello "
是在try
块中执行时打印的。"throwit "
是在调用throwit()
方法时打印的。"caught "
是在catch
块中捕获异常后打印的。"finally "
是在finally
块中执行的。"after "
是在finally
块执行完后,main
方法中最后一行代码打印的。
因此,最终输出是:
hello throwit caught finally after
10. class A
{
public A(int x){}
}
class B extends A { }
public class test
{
public static void main (String args [])
{
A a = new B();
System.out.println("complete");
}
}
### 代码分析:
让我们逐步解析这段代码的行为:
```java
class A {
public A(int x) {
// 构造函数 A,接受一个整数参数
}
}
class B extends A {
// 类 B 继承自类 A
// 由于类 A 的构造函数需要一个 int 参数,B 必须调用 A 的构造函数。
}
public class test {
public static void main(String args[]) {
A a = new B(); // 创建一个 B 类型的对象,并赋值给 A 类型的引用
System.out.println("complete");
}
}
关键点分析:
-
类 A 的构造函数:
class A
中定义了一个构造函数public A(int x)
,它接受一个int
参数。- 这意味着类
A
的对象必须通过传递一个整数来进行初始化。
-
类 B 继承自类 A:
- 类
B
继承自A
,但没有定义自己的构造函数。 - 因为类
B
没有显式定义构造函数,编译器会默认生成一个无参构造函数。
- 类
-
问题所在:
- 在
class B
中,默认的无参构造函数会自动调用super()
,即A
类的无参构造函数。 - 然而,
class A
中并没有提供无参构造函数,只有一个带int
参数的构造函数。所以,B
的默认无参构造函数无法正常编译。
- 在
-
解决方法:
- 由于
A
类没有无参构造函数,类B
必须显式调用super(int x)
,并提供一个int
参数给类A
的构造函数。
- 由于
修改后的代码:
class A {
public A(int x) {
// 构造函数 A,接受一个整数参数
System.out.println("A's constructor with int: " + x);
}
}
class B extends A {
public B() {
super(10); // 显式调用 A 的构造函数,传递一个整数
System.out.println("B's constructor");
}
}
public class Test {
public static void main(String args[]) {
A a = new B(); // 创建 B 的对象,会调用 B 的构造函数
System.out.println("complete");
}
}
输出结果:
A's constructor with int: 10
B's constructor
complete
总结:
在原代码中,类 B
没有显式调用 super(int x)
,导致编译错误。解决方法是,在 B
的构造函数中显式调用 super(10)
,并传递一个合适的参数,这样程序就可以正常编译和运行。
11. String x = "xyz";
x.toUpperCase(); /* Line 2 */
String y = x.replace('Y', 'y');
y = y + "abc";
System.out.println(y);
程序的输出是
让我们逐步分析这段代码,了解每一行的作用以及最终输出。
String x = "xyz"; // 第1行,定义一个字符串变量 x 并赋值为 "xyz"
x.toUpperCase(); // 第2行,调用 toUpperCase() 方法,但结果没有保存
String y = x.replace('Y', 'y'); // 第3行,尝试将字符 'Y' 替换为 'y',但 'Y' 不在字符串中
y = y + "abc"; // 第4行,将 "abc" 拼接到字符串 y 上
System.out.println(y); // 输出 y 的值
逐行解析:
-
第1行:
String x = "xyz";
- 这行定义了一个字符串
x
,并将其初始化为"xyz"
。
- 这行定义了一个字符串
-
第2行:
x.toUpperCase();
toUpperCase()
方法将返回一个将字符串全部转为大写的新字符串。- 但是,这里 没有将返回值赋给任何变量,所以这个操作没有实际影响。
- 原字符串
x
依然是"xyz"
,并没有改变。
-
第3行:
String y = x.replace('Y', 'y');
replace('Y', 'y')
方法会尝试将x
字符串中的字符'Y'
替换为'y'
。- 但是,字符串
"xyz"
中并没有字符'Y'
(它是小写的'y'
),因此replace
操作不会做任何替换,y
仍然是"xyz"
。
-
第4行:
y = y + "abc";
- 这行代码将
"abc"
字符串拼接到y
字符串的末尾。 - 由于
y
是"xyz"
,所以拼接后,y
的值变为"xyzabc"
。
- 这行代码将
-
第5行:
System.out.println(y);
- 这行代码将打印
y
的值,即"xyzabc"
。
- 这行代码将打印
输出结果:
xyzabc
总结:
x.toUpperCase();
没有影响,因为返回的值没有被保存。x.replace('Y', 'y');
没有进行任何替换,因为"xyz"
中没有大写字母'Y'
。- 最终,
y
13. public void foo( boolean a, boolean b)
{
if( a )
{
System.out.println("A"); /* Line 5 */
}
else if(a && b) /* Line 7 */
{
System.out.println( "A && B");
}
else /* Line 11 */
{
if ( !b )
{
System.out.println( "notB") ;
}
else
{
System.out.println( "ELSE" ) ;
}
}
}
A.如果 a 为真且 b 为真,则输出为“A && B”
B.如果 a 为真且 b 为假,则输出为“notB”
C.如果 a 为假且 b 为真,则输出为“ELSE”
D.如果 a 为假且 b 为假,则输出为“ELSE”
让我们逐步分析这段代码,并确定每种情况下的输出。
### 代码分析:
public void foo(boolean a, boolean b) {
if (a) { // Line 3
System.out.println("A"); // Line 5
}
else if (a && b) { // Line 7
System.out.println("A && B");
}
else { // Line 11
if (!b) {
System.out.println("notB");
}
else {
System.out.println("ELSE");
}
}
}
逐步分析:
1. 如果 a
为真且 b
为真
- 当
a
为true
时,进入第一个if (a)
语句块。 - 无论
b
是什么值,if (a)
会执行,输出"A"
。
输出:
A
2. 如果 a
为真且 b
为假
- 当
a
为true
,b
为false
时,进入第一个if (a)
语句块。 - 无论
b
的值,if (a)
会执行,输出"A"
。
输出:
A
3. 如果 a
为假且 b
为真
- 当
a
为false
,b
为true
时,进入else if (a && b)
。 - 由于
a
为false
,a && b
会返回false
,因此跳过这个else if
块,进入else
语句。 - 在
else
语句中,检查if (!b)
:!b
为false
,因为b
为true
。- 所以执行
else
块,输出"ELSE"
。
输出:
ELSE
4. 如果 a
为假且 b
为假
- 当
a
为false
,b
为false
时,进入else if (a && b)
。 - 同样地,
a && b
为false
,跳过这个else if
,进入else
块。 - 在
else
语句中,检查if (!b)
:!b
为true
,因为b
为false
。- 所以执行
if (!b)
块,输出"notB"
。
输出:
notB
总结选项:
- A: 如果
a
为真且b
为真,则输出为"A"
,因为if (a)
会被执行,无论b
是否为真。 - B: 如果
a
为真且b
为假,则输出为"A"
,因为if (a)
会被执行。 - C: 如果
a
为假且b
为真,则输出为"ELSE"
,因为进入else
块后执行"ELSE"
。 - D: 如果
a
为假且b
为假,则输出为"notB"
,因为if (!b)
会被执行。
最终答案:
- A: 错误 — 因为
a
为真时,输出"A"
。 - B: 错误 — 因为
a
为真时,输出"A"
,不需要检查b
。 - C: 正确 — 当
a
为假且b
为真时,输出"ELSE"
。 - D: 正确 — 当
a
为假且b
为假时,输出"notB"
。
*14. class Bitwise
{
public static void main(String [] args)
{
int x = 11 & 9;
int y = x ^ 3;
System.out.println( y | 12 );
}
}
让我们逐步分析这段代码,理解每个位运算的操作,并确定最终的输出。
代码分析:
class Bitwise {
public static void main(String [] args) {
int x = 11 & 9; // 第1步
int y = x ^ 3; // 第2步
System.out.println(y | 12); // 第3步
}
}
步骤 1: x = 11 & 9
-
这是一个按位与 (
&
) 操作。 -
首先,我们将
11
和9
转换为二进制:11
的二进制表示是1011
。9
的二进制表示是1001
。
1011 1001 ---- 1001 // 结果是按位与操作后的值
解释:按位与 (
&
) 操作是逐位比较两个二进制数的对应位。如果两个对应位都为 1,则结果为 1,否则为 0。所以:1011 & 1001 ----- 1001 // 结果是 9
-
因此,
x = 9
。
步骤 2: y = x ^ 3
-
这是一个按位异或 (
^
) 操作。 -
x
现在是9
,我们将9
和3
转换为二进制:9
的二进制表示是1001
。3
的二进制表示是0011
。
1001 0011 ---- 1010 // 结果是按位异或操作后的值
解释:按位异或 (
^
) 操作是逐位比较两个二进制数的对应位。如果对应位相同,则结果为 0;如果不同,则结果为 1。所以上述操作结果为:1001 ^ 0011 ----- 1010 // 结果是 10
-
因此,
y = 10
。
步骤 3: y | 12
-
这是一个按位或 (
|
) 操作。 -
y
现在是10
,我们将10
和12
转换为二进制:10
的二进制表示是1010
。12
的二进制表示是1100
。
1010 1100 ---- 1110 // 结果是按位或操作后的值
解释:按位或 (
|
) 操作是逐位比较两个二进制数的对应位。如果两个对应位中至少有一个为 1,则结果为 1,否则为 0。所以:1010 | 1100 ----- 1110 // 结果是 14
-
因此,
y | 12 = 14
。
最终输出:
程序的最终输出是 14
。
结论:
14
15. public class X
{
public static void main(String [] args)
{
String names [] = new String[5];
for (int x=0; x < args.length; x++)
names[x] = args[x];
System.out.println(names[2]);
}
}
运行如下命令:java X a b
程序的输出是?
让我们逐步分析这段代码,并解释程序的执行过程。
代码分析:
public class X {
public static void main(String [] args) {
String names [] = new String[5]; // 第1步
for (int x = 0; x < args.length; x++) // 第2步
names[x] = args[x]; // 第3步
System.out.println(names[2]); // 第4步
}
}
步骤 1: String names[] = new String[5];
- 这里定义了一个
String
类型的数组names
,并为它分配了长度为5
的空间。 - 这意味着数组
names
可以存储 5 个字符串元素,初始化时,数组中的每个元素的值是null
。
步骤 2: for (int x = 0; x < args.length; x++)
- 这个循环遍历命令行参数
args
,并将这些参数依次赋值给names
数组。 args.length
是传入程序的命令行参数的数量。根据问题描述,命令行参数是a
和b
。
步骤 3: names[x] = args[x];
- 这个循环的作用是将
args
数组中的元素复制到names
数组的对应位置。
对于命令行输入 java X a b
:
args[0]
是"a"
,args[1]
是"b"
,args.length
是 2。- 因此,
names[0] = "a"
和names[1] = "b"
,其他位置(names[2]
、names[3]
、names[4]
)仍然是null
。
步骤 4: System.out.println(names[2]);
- 这里,程序打印
names[2]
。 - 根据上面的分析,
names[2]
没有被赋值,因此它的值是null
。
运行命令:
命令是 java X a b
,即传入了两个命令行参数:a
和 b
。因此:
args[0]
是"a"
。args[1]
是"b"
。args[2]
、args[3]
、args[4]
默认值为null
。
结果:
程序会打印 names[2]
,但是 names[2]
为 null
,因此输出:
null
结论:
null
*16. 您希望一个类可以访问同一个包中另一个类的成员。 实现这一目标的最严格的访问是什么?
A.public
B. private
C. protected
D. default access
在Java中,类的成员(如字段、方法等)的访问权限通过访问修饰符进行控制。问题要求你选择最严格的访问权限,以便一个类能够访问同一个包中的另一个类的成员。
我们逐一分析选项:
A. public
public
访问修饰符表示成员可以被任何其他类访问,不受包或继承结构的限制。- 这意味着一个类可以从任何包中访问该成员,因此这不是最严格的访问权限。
B. private
private
访问修饰符表示成员只能在其所属类内部访问,其他类(即使在同一个包中)也无法访问。- 因此,
private
不是一个适合在同一个包中访问的权限。它是最严格的访问控制,但不符合问题中的需求。
C. protected
protected
访问修饰符意味着成员可以被同一个包中的类以及所有继承该类的子类访问。- 但对于同一个包中的类,
protected
的权限并不严格,因此它不是最严格的访问修饰符。
D. default access (包访问权限)
- 当没有显式指定访问修饰符时,Java 中的成员具有 默认访问权限,即“包访问权限”(也称为 包级私有)。
- 这意味着成员只能被同一个包中的其他类访问,不能被包外的类访问,也不能被子类在不同包中访问。
- 这是一种相对严格的访问权限,因为它限制了类成员的访问范围,但仍允许同一包中的其他类访问。
正确答案:
最严格的访问控制是 private
,但问题要求的是“同一个包中”能够访问成员。为了能够在同一个包中访问而不暴露给其他包,最严格的控制是 默认访问权限(即没有指定访问修饰符)。
因此,最严格的访问权限,同时允许同一个包内的其他类访问,是 D. default access。
17. 程序的输出是?
public class Test107 implements Runnable
{
private int x;
private int y;
public static void main(String args[])
{
Test107 that = new Test107();
(new Thread(that)).start();
(new Thread(that)).start();
}
public synchronized void run()
{
for(int i = 0; i < 10; i++)
{
x++;
y++;
System.out.println("x = " + x + ", y = " + y); /* Line 17 */
}
}
}
A.编译错误
B. 以如下顺序打印: x = 1 y = 1 x = 2 y = 2 x = 3 y = 3 x = 4 y = 4 x = 5 y = 5... 但输出将由同时运行的两个线程产生.
C. 以如下顺序打印: x = 1 y = 1 x = 2 y = 2 x = 3 y = 3 x = 4 y = 4 x = 5 y = 5... 但输出将由第一个线程产生,然后由另一个线程产生。 这是由同步代码保证的。
D. 以如下顺序打印: x = 1 y = 2 x = 3 y = 4 x = 5 y = 6 x = 7 y = 8...
代码分析
我们来看这段代码:
public class Test107 implements Runnable {
private int x;
private int y;
public static void main(String args[]) {
Test107 that = new Test107();
(new Thread(that)).start(); // 创建并启动第一个线程
(new Thread(that)).start(); // 创建并启动第二个线程
}
public synchronized void run() {
for (int i = 0; i < 10; i++) {
x++; // 增加 x 的值
y++; // 增加 y 的值
System.out.println("x = " + x + ", y = " + y); // 打印当前的 x 和 y
}
}
}
关键点:
- 类
Test107
实现了Runnable
接口,并且run()
方法是 synchronized 的。 main
方法中创建了两个线程,并分别启动它们。- 这两个线程共享同一个
Test107
实例,因此它们会同时执行run()
方法中的代码。 - 由于
run()
方法是 synchronized 的,只有一个线程可以在任何时刻进入run()
方法,另一个线程必须等待当前线程执行完毕后才能进入。
分析每个选项:
A. 编译错误
- 代码不会编译出错,因为
Test107
正确实现了Runnable
接口,且同步机制也正确使用。因此,选项 A 不是正确答案。
B. 以如下顺序打印: x = 1 y = 1 x = 2 y = 2 x = 3 y = 3 x = 4 y = 4 x = 5 y = 5… 但输出将由同时运行的两个线程产生。
- 这个描述不完全准确。虽然
run()
方法在多个线程中执行,但由于run()
是 synchronized 的,所以 两个线程不能同时执行run()
方法。它们会交替执行,且每次只有一个线程可以打印x
和y
的值。因此,输出将会由一个线程按顺序打印,另一个线程在第一个线程执行完当前循环的迭代后才会执行。
C. 以如下顺序打印: x = 1 y = 1 x = 2 y = 2 x = 3 y = 3 x = 4 y = 4 x = 5 y = 5… 但输出将由第一个线程产生,然后由另一个线程产生。这是由同步代码保证的。
- 这个选项描述了同步的行为是正确的。由于
run()
方法是同步的,两个线程会交替执行每个循环,而每个线程对x
和y
的增加都是互斥的。 - 因此,输出将由一个线程先连续打印出 10 次
x = n, y = n
,然后第二个线程打印接下来的 10 次输出。最终的输出顺序将是由两个线程交替产生的。
D. 以如下顺序打印: x = 1 y = 2 x = 3 y = 4 x = 5 y = 6 x = 7 y = 8…
- 这个选项是不正确的。由于每个线程每次都递增
x
和y
,它们会同步递增,而不是x
和y
交替递增。因此,x
和y
的值会是相等的,而不是交替的。
结论
正确的答案是 C。
因为 run()
方法是 synchronized 的,两个线程会交替执行,并且每次执行时都会按顺序打印出 x
和 y
的递增值,而不会发生并发问题。
在Java中,synchronized
是一个关键字,用于实现 同步(synchronization)。它用于控制对共享资源的访问,以确保多个线程在执行某段代码时,能够按照一定的顺序进行,从而避免并发问题。
1. synchronized
的作用
当一个方法或代码块被声明为 synchronized
时,Java 会确保 同一时刻只有一个线程 能够执行这段同步代码。其他线程必须等待当前线程执行完毕才能进入同步方法或同步代码块。
这对于防止多个线程同时访问并修改共享资源(比如变量、数据结构等)时引发的数据不一致或竞争条件(race condition)非常重要。
2. 如何使用 synchronized
a. 同步方法
synchronized
可以用于方法的声明,表示该方法是同步的。
public synchronized void exampleMethod() {
// 只有一个线程可以在任何时刻执行这个方法
x++; // 访问共享资源
y++;
System.out.println(x + ", " + y);
}
在这种情况下,同一时刻只能有一个线程 进入 exampleMethod()
方法。其他线程在方法执行完之前,必须等待。
b. 同步代码块
synchronized
还可以用于方法内部的代码块。通过指定一个对象锁,线程在执行同步代码块时,必须首先获得该对象的锁。
public void exampleMethod() {
synchronized(this) {
// 只有一个线程可以在任何时刻执行这个代码块
x++;
y++;
System.out.println(x + ", " + y);
}
}
在这个例子中,只有获得 this
对象锁的线程才能执行 synchronized
代码块。如果多个线程同时调用该方法,它们必须争夺 this
对象的锁。
c. 静态同步方法
synchronized
还可以用于类的静态方法,表示该方法在类级别上进行同步。
public static synchronized void exampleStaticMethod() {
// 同样,只有一个线程可以执行这个静态同步方法
}
静态同步方法使用 类对象(而不是实例对象)作为锁。
3. 为什么使用 synchronized
Java中的多线程程序常常需要访问共享资源。如果多个线程同时访问共享资源且没有适当的同步机制,就可能会发生并发问题,比如:
- 数据不一致:多个线程同时修改共享变量,导致无法预测的结果。
- 竞态条件(Race Condition):两个线程竞争某些资源,导致程序表现出不稳定的行为。
synchronized
通过使得同一时刻只有一个线程能执行关键代码,从而避免这些并发问题。
4. synchronized
的工作原理
当多个线程试图执行一个同步方法时,锁机制会确保同一时刻只有一个线程可以进入该方法或代码块。锁可以是:
- 实例锁:针对对象实例(
this
),通常是方法中的同步块或非静态方法。 - 类锁:针对类本身,通常是静态方法。
举例说明:
假设有两个线程 T1
和 T2
,它们同时调用一个同步方法。由于同步方法的锁机制,T1
会先获取锁并执行该方法,而 T2
必须等到 T1
执行完后才能进入该方法。
5. 性能考虑
虽然 synchronized
关键字能有效解决线程同步问题,但它也带来了一些性能开销。因为:
- 获取和释放锁需要时间。
- 当多个线程频繁访问同步方法时,会出现线程竞争,可能导致线程的阻塞和上下文切换,影响性能。
6. 小结
synchronized
是一种用于实现线程同步的机制,确保在多线程环境下对共享资源的访问是安全的。- 它可以用在方法和代码块上。
- 使用
synchronized
会引入一定的性能开销,因此需要小心使用,尤其是在高并发场景中。
下列说法正确的是
A.有时明确抛出 AssertionError 是一种很好的做法。
B. 私有 getter() 和 setter() 方法不应使用断言来验证参数。
C. 如果在 try-catch 块中抛出 AssertionError,finally 块将被绕过。
D. 使用 catch (AssertionException ae) 块处理断言语句失败是正确的
我们来看一下每个选项的分析,并解释正确与否:
A. 有时明确抛出 AssertionError 是一种很好的做法。
-
错误。一般来说,断言(assertion) 用于开发阶段进行内部错误检查,确认程序的假设条件,而不是用作正常的错误处理机制。
AssertionError
通常是在程序不符合预期时自动抛出的,而不应该主动抛出。主动抛出AssertionError
会混淆代码的意图和错误的处理方式,可能会让代码不易理解和维护。正确做法:
assert
语句本身用于断言条件,不应该通过代码显式抛出AssertionError
。如果需要进行错误处理,应使用适当的异常(如IllegalArgumentException
等)。
B. 私有 getter() 和 setter() 方法不应使用断言来验证参数。
- 正确。
getter()
和setter()
方法通常是公开访问对象状态的方式,而断言的目的是用于验证假设条件,通常是在开发过程中用于调试和检测内部错误。对于公共方法的参数验证,应该使用异常处理来进行,而不是使用断言。- 断言 在生产环境中默认是关闭的,所以依赖断言进行参数验证会导致程序行为不可预测。应该使用如
IllegalArgumentException
等标准异常来验证方法参数。
- 断言 在生产环境中默认是关闭的,所以依赖断言进行参数验证会导致程序行为不可预测。应该使用如
C. 如果在 try-catch 块中抛出 AssertionError,finally 块将被绕过。
-
错误。无论
try
块中的代码抛出什么异常,finally
块总是会被执行。finally
块无论是否发生异常都会执行,除非程序在执行期间被终止(如调用System.exit()
或发生严重错误导致JVM崩溃)。- 所以,即使在
try-catch
块中抛出AssertionError
,finally
仍然会执行。finally
块的主要目的是释放资源等清理工作。
- 所以,即使在
D. 使用 catch (AssertionException ae) 块处理断言语句失败是正确的
-
错误。Java 中并没有
AssertionException
这个异常类。assert
语句失败时会抛出一个AssertionError
,而不是AssertionException
。因此,应该使用catch (AssertionError ae)
来捕获断言失败抛出的错误,而不是AssertionException
。另外,断言失败的目的是标识程序中的逻辑错误,通常不应该通过
try-catch
块来捕获它。一般情况下,AssertionError
应该是由程序员在调试时用来检查假设错误的,正常的错误处理应该使用合适的异常类型。
总结:
- A. 错误:不应显式抛出
AssertionError
。 - B. 正确:不应使用断言来验证
getter
和setter
方法的参数。 - C. 错误:
finally
块无论如何都会执行。 - D. 错误:应该捕获
AssertionError
,而不是AssertionException
。
正确的答案是:B。
下列说法正确的是
A.调用 Runtime.gc() 将导致符合条件的对象被垃圾回收。
B.垃圾收集器使用标记和清除算法。
C. 如果一个对象可以从一个活动线程中访问,它就不能被垃圾回收。
D. 如果对象 1 引用对象 2,则对象 2 不能被垃圾回收
让我们分析每个选项,找出正确的说法。
A. 调用 Runtime.gc() 将导致符合条件的对象被垃圾回收。
- 错误。
Runtime.gc()
提示 JVM 进行垃圾回收,但它并不是强制性的,并不能保证垃圾回收一定会发生。调用gc()
方法只是给垃圾收集器发出了建议,实际上,是否执行垃圾回收、何时执行垃圾回收完全取决于垃圾收集器的实现和当前的内存状况。垃圾回收的决定是由 JVM 根据内存压力来做的,而不是单纯由gc()
方法的调用决定。
B. 垃圾收集器使用标记和清除算法。
-
正确。标记-清除(Mark-and-Sweep)是垃圾回收的一种经典算法。其过程通常如下:
- 标记阶段:首先标记所有活动对象,这些对象是可达的(即从根对象可访问的对象)。
- 清除阶段:在标记之后,垃圾收集器会回收那些没有被标记的对象,即不可达的对象。
这种算法的优点是简单易懂,但也有一些缺点,比如会导致内存碎片问题。现代的垃圾收集器(如 G1、ZGC 等)通常在标记-清除的基础上进行了优化。
C. 如果一个对象可以从一个活动线程中访问,它就不能被垃圾回收。
- 正确。如果一个对象是可达的(即可以通过活动线程或者其他任何途径访问到该对象),那么它不会被垃圾回收器回收。垃圾回收的主要依据是对象是否可达(reachable)。如果对象不再被任何活动线程或其他对象引用,那么它就变成了不可达对象,有可能会被垃圾回收。
D. 如果对象 1 引用对象 2,则对象 2 不能被垃圾回收。
- 正确。如果对象 1 引用了对象 2,这意味着对象 2 是可达的。因此,对象 2 在垃圾回收时不会被回收,直到对象 1 不再引用对象 2 为止。只有那些没有任何引用(即不可达)的对象,才会被垃圾回收。
总结:
- A. 错误:
Runtime.gc()
只是一个建议,不保证垃圾回收会发生。 - B. 正确:垃圾收集器使用标记和清除算法。
- C. 正确:如果对象可以从活动线程中访问,它就不能被垃圾回收。
- D. 正确:如果对象 1 引用对象 2,对象 2 就不会被垃圾回收。
正确的答案是:B, C, D。
20. public class Test
{
public void foo()
{
assert false; /* Line 5 */
assert false; /* Line 6 */
}
public void bar()
{
while(true)
{
assert false; /* Line 12 */
}
assert false; /* Line 14 */
}
}
哪一个会导致编译失败
A.Line 5
B. Line 6
C. Line 12
D. Line 14
让我们仔细分析代码中涉及断言(assert
)的部分。
public class Test
{
public void foo()
{
assert false; /* Line 5 */
assert false; /* Line 6 */
}
public void bar()
{
while(true)
{
assert false; /* Line 12 */
}
assert false; /* Line 14 */
}
}
断言(assert
)的工作原理
在 Java 中,断言是通过 assert
语句实现的,通常用于在开发过程中检查程序的假设条件。断言默认在运行时是禁用的,只有通过 -ea
(-enableassertions
)选项显式启用时,断言才会执行。如果没有启用断言,assert
语句将不会影响程序的行为。
编译阶段:
assert
语句本身并不会导致编译错误。编译器会接受包含assert
语句的代码,只要没有语法错误。
运行时:
- 只有在运行时启用断言时,
assert
语句才会生效。否则,它们会被忽略。
逐行分析
-
Line 5 和 Line 6 (
assert false;
)- 这些是标准的断言语句,不会导致编译错误。无论是启用断言还是禁用断言,都会被编译器接受。
-
Line 12 (
assert false;
)- 在
while(true)
循环内部,assert false
不会导致编译失败。assert
语句本身是有效的。循环体内部的断言会在循环中每次执行时被执行,但它不会导致编译错误。
- 在
-
Line 14 (
assert false;
)- 这一行是位于
while(true)
循环外部的断言。这不会导致编译错误,除非有语法问题或者错误的使用方式。但是在 Java 中,assert
语句本身并不会因其位置或上下文而导致编译失败。
- 这一行是位于
结论
没有一行代码会导致编译错误。Java 编译器会接受所有的 assert
语句。关键在于是否启用了断言(-ea
),但即使不启用,assert
语句本身也不会阻止程序编译。
正确答案是: 没有选项会导致编译失败。
- 请用java实现一个函数。输入是一个数组,数组的元素都是数字,里面的数字有重复出现的。函数返回一个新的数组,是对输入数组去重的结果??????
package org.example;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
public class RemoveDuplicates {
public static int[] removeDuplicates(int[] inputArray) {
// 创建一个 HashSet 来去重
Set<Integer> uniqueSet = new HashSet<>();
// 将数组元素添加到 HashSet 中,自动去除重复元素
for (int num : inputArray) {
uniqueSet.add(num);
}
// 将 Set 转换为新的数组
int[] resultArray = new int[uniqueSet.size()];
int index = 0;
for (int num : uniqueSet) {
resultArray[index++] = num;
}
// 返回去重后的数组
return resultArray;
}
public static void main(String[] args) {
// 示例输入
// int[] inputArray = {1, 2, 2, 3, 4, 4, 5, 5, 5};
Scanner scanner = new Scanner(System.in);
System.out.println("请输入数组长度");
int n = scanner.nextInt();
int[] inputArray = new int[n];
System.out.println("请输入数组元素");
for (int i = 0; i < n; i++) {
inputArray[i] = scanner.nextInt();
}
// 调用去重函数
int[] resultArray = removeDuplicates(inputArray);
// 输出结果
System.out.print("去重后的数组: ");
for (int num : resultArray) {
System.out.print(num + " ");
}
}
}
在 Java 中,线程的生命周期包括多个不同的状态。Java 线程的状态是通过 Thread.State
枚举类来定义的。下面是 Java 线程的几种常见状态:
1. NEW (新建)
- 线程刚创建,但还未开始执行时的状态。
- 线程对象被创建之后,调用
start()
方法之前,线程处于此状态。 - 例如:
Thread thread = new Thread(); System.out.println(thread.getState()); // 输出:NEW
2. RUNNABLE (可运行)
- 线程已经启动,但还未开始执行,或者正在执行的状态。
- 线程在操作系统的线程调度器中处于等待执行的队列中。当 CPU 分配时间片后,线程将变为
RUNNABLE
状态。 - 注意:Java 中没有明确的 Running 状态,只有 RUNNABLE 状态。即使线程正在运行,它仍然处于
RUNNABLE
状态。 - 例如,线程调用
start()
后进入RUNNABLE
状态。Thread thread = new Thread(() -> System.out.println("Running...")); thread.start(); // 线程进入 RUNNABLE 状态
3. BLOCKED (阻塞)
- 线程因尝试获取一个对象的锁而被阻塞,直到能够成功获取锁。
- 当一个线程想访问同步方法或同步块时,如果其他线程已经持有锁,当前线程将被阻塞,直到锁可用为止。
- 例如:
synchronized (lock) { // 当前线程如果无法获得 lock 锁,就会进入 BLOCKED 状态 }
4. WAITING (等待)
- 线程正在等待其他线程执行特定操作,通常是调用
Object.wait()
方法后进入此状态,或者调用Thread.join()
等方法等待其他线程完成。 - 线程进入
WAITING
状态后,必须等待其他线程通过notify()
或notifyAll()
方法唤醒它。 - 例如:
synchronized (lock) { lock.wait(); // 当前线程进入 WAITING 状态 }
5. TIMED_WAITING (定时等待)
- 线程在一个固定的时间段内等待。线程调用
Thread.sleep(milliseconds)
,Object.wait(time)
,Thread.join(time)
等方法时进入此状态。 - 线程会等待指定的时间,超时后自动回到
RUNNABLE
状态。 - 例如:
Thread.sleep(1000); // 线程进入 TIMED_WAITING 状态,等待 1000 毫秒
6. TERMINATED (已终止)
- 线程执行完毕或由于异常终止时的状态。
- 当线程的
run()
方法执行完毕或线程因未捕获的异常退出时,它进入TERMINATED
状态。 - 线程不能再返回到其他状态,已经结束生命周期。
- 例如:
Thread thread = new Thread(() -> { // 执行完毕 }); thread.start(); // 等待线程终止 thread.join(); // 线程进入 TERMINATED 状态
总结:线程状态的转换
- NEW →
start()
→ RUNNABLE(进入就绪队列等待 CPU 分配) - RUNNABLE →
synchronized
或join()
等 → BLOCKED - RUNNABLE →
wait()
→ WAITING - RUNNABLE →
sleep()
、join(time)
等 → TIMED_WAITING - RUNNABLE → 执行完毕或因异常退出 → TERMINATED
状态图
NEW
|
v
RUNNABLE <-- BLOCKED
| ^
v |
WAITING <---- TIMED_WAITING
|
v
TERMINATED
这些是 Java 线程生命周期中的主要状态,它们由 JVM 和操作系统的线程调度机制共同管理。