java-questions-分析

Louis yeap 2024-09-02 15:03:01 阅读 100

系列文章目录


文章目录

目录

系列文章目录

文章目录

前言

一、问题案例

1、maven项目compile时候出现告警warn

目录

系列文章目录

文章目录

前言

一、问题案例

1、maven项目compile时候出现告警warn

2、java文件打包然后在命令行中运行java会找不到主类

3、程序找不到数据库驱动和配置实例

4、springboot和mybatis-plus版本不兼容导致

5、springboot项目启动的解释

6、jdbc驱动过时

7、springboot项目启动时候提示:Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

8、Druid监控页面无法打开(404)

9、Arrays.asList()是泛型方法,传递的数组必须是对象数组,而不是基本类型。

10、for循环操作集合异常

11、java的值传递(数组/集合 && 对象)

总结


前言


一、问题案例

1、maven项目compile时候出现告警warn

告警提示:

Parameter 'compilerVersion' (user property 'maven.compiler.compilerVersion') is deprecated: This parameter is no longer evaluated by the underlying compilers, instead the actual version of the javac binary is automatically retrieved. [INFO] Recompiling the module because of changed source code.

解决:

这个警告消息来自 Maven 编译器插件,提示参数 compilerVersion 已被弃用,并且不再由底层编译器评估。相反,Javac 二进制文件的实际版本会自动检索。

compilerVersion 参数已被弃用,这意味着即使你在 Maven 配置中指定了这个参数,它也不会影响编译器的行为。Maven 编译器插件会自动确定并使用已安装的 Java 编译器版本。

解决方法一:

如果你在 pom.xml 文件中指定了 compilerVersion 参数,可以安全地移除它。相反,应该确保你正确配置了 sourcetarget 参数来指定编译的 Java 版本。

解决方法二:

添加这两句解决问题是因为它们明确地指定了 Maven 编译器插件的配置。具体来说:

```xml

<plugin>

    <groupId>org.apache.maven.plugins</groupId>

    <artifactId>maven-compiler-plugin</artifactId>

    <version>3.8.1</version> <!-- 确保使用最新版本 -->

    <configuration>

        <source>${java.version}</source>

        <target>${java.version}</target>

    </configuration>

</plugin>

```

1. **`maven-compiler-plugin` 的配置**:- `maven-compiler-plugin` 是 Maven 用于编译 Java 源代码的插件。它负责将源代码编译成字节码。

   - 指定插件的版本(例如 `3.8.1`)确保你使用的是最新的稳定版本,这样可以避免已知的错误或不兼容问题。

2. **`<source>` 和 `<target>` 参数**:

   - 这两个参数用来指定 Java 语言的版本。

   - `<source>` 定义了编译源代码时使用的 Java 版本。

   - `<target>` 定义了生成字节码时的目标 Java 版本。

   - 例如,如果你的项目使用 Java 17,`<source>` 和 `<target>` 都应该设置为 `17`。

   - 这些设置帮助 Maven 编译器插件确定要使用的 Java 语言特性和字节码格式。

### 解决问题的原因

- **明确的编译器设置**: 当你指定 `<source>` 和 `<target>` 版本时,Maven 编译器插件知道你想要使用哪种 Java 版本。这消除了插件猜测或自动检测的必要性,并且避免了可能的警告或错误。

- **兼容性和稳定性**: 使用明确指定的版本(例如 `3.8.1`)可以确保你利用的是经过验证的版本,而不是不稳定或过时的版本。这也帮助避免了由于使用不同的插件版本而引起的不兼容问题。

- **清晰的项目配置**: 通过明确的配置,其他开发者或构建系统在构建你的项目时不需要做额外的配置调整。项目的构建过程变得更加可预测和稳定。通过添加这些配置,你明确告诉 Maven 如何编译你的 Java 代码。这不仅可以解决特定的警告或错误,还可以确保项目在不同环境和设置下的一致性和稳定性。

2、java文件打包然后在命令行中运行java会找不到主类

原因:

要确保 Java 运行时能找到你的主类,你需要在打包时正确地指定主类。可以通过两种方式做到这一点

解决:

### 1. **指定主类**

要确保 Java 运行时能找到你的主类,你需要在打包时正确地指定主类。可以通过两种方式做到这一点:

#### 在 `pom.xml` 中指定主类

如果你使用 Maven 构建项目,可以在 `pom.xml` 中通过 `maven-jar-plugin` 指定主类:

```xml

<build>

    <plugins>

        <plugin>

            <groupId>org.apache.maven.plugins</groupId>

            <artifactId>maven-jar-plugin</artifactId>

            <version>3.2.0</version>

            <configuration>

                <archive>

                    <manifest>

                        <addClasspath>true</addClasspath>

                        <classpathPrefix>lib/</classpathPrefix>

                        <mainClass>com.example.MainClass</mainClass> <!-- 替换为你的主类 -->

                    </manifest>

                </archive>

            </configuration>

        </plugin>

    </plugins>

</build>

```

- `<mainClass>` 元素指定了你的主类的完全限定名(包括包名)。

#### 手动创建 `MANIFEST.MF` 文件

在构建过程中,或手动创建一个包含以下内容的 `MANIFEST.MF` 文件:

```

Manifest-Version: 1.0

Main-Class: com.example.MainClass

```

然后将此文件包含在你的 JAR 文件中。确保 `com.example.MainClass` 是你的主类的全限定名。

### 2. **打包 JAR 文件**

确保你正确地打包了 JAR 文件并且所有的类文件和资源都包含在内。例如,使用 Maven,可以运行以下命令:

```bash

mvn clean package

```

这个命令会生成一个包含指定主类的可执行 JAR 文件。

### 3. **运行 JAR 文件**

一旦你有了正确的 JAR 文件,可以使用以下命令运行它:

```bash

java -jar your-jar-file.jar

```

确保 `your-jar-file.jar` 是你生成的 JAR 文件的名称。

### 4. **检查包名和类路径**

如果你仍然遇到问题,检查以下几点:

- **主类的包名**: 确保主类的包名在代码和 JAR 文件中是一致的。

- **类路径**: 如果你的应用程序依赖外部库,请确保它们在运行时的类路径中。如果它们没有打包在 JAR 文件中,你可能需要通过 `-classpath` 或 `-cp` 选项指定它们。

例如:

```bash

java -cp your-jar-file.jar:lib/* com.example.MainClass

```

这会在类路径中包括 `your-jar-file.jar` 和 `lib` 目录中的所有 JAR 文件。

通过这些步骤,你应该能够正确地指定和找到主类,并成功运行你的 Java 应用程序。

3、程序找不到数据库驱动和配置实例

问题和解决:

Caused by: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class

这个错误信息 `Failed to determine a suitable driver class` 表示 Spring Boot 在尝试创建数据源时,无法确定合适的数据库驱动类。这通常是由于以下几个原因之一:

### 1. **缺少数据库驱动依赖**

确保你的 `pom.xml` 中已经包含了相应的数据库驱动程序。例如,如果你使用 MySQL 数据库,应该包含以下依赖:

```xml

<dependency>

    <groupId>com.mysql</groupId>

    <artifactId>mysql-connector-j</artifactId>

    <version>8.0.32</version> <!-- 使用你需要的版本 -->

</dependency>

```

### 2. **未配置正确的数据库连接属性**

检查 `application.properties` 或 `application.yml` 中的数据库连接配置,确保已提供正确的 JDBC URL、用户名和密码。对于 MySQL,配置应如下所示:

```properties

spring.datasource.url=jdbc:mysql://localhost:3306/yourdatabase

spring.datasource.username=yourusername

spring.datasource.password=yourpassword

```

或在 `application.yml` 中:

```yaml

spring:

  datasource:

    url: jdbc:mysql://localhost:3306/yourdatabase

    username: yourusername

    password: yourpassword

```

确保 `jdbc:mysql://` 是你的数据库 URL 的前缀。

### 3. **不正确的驱动类名称**

尽管 Spring Boot 通常会自动检测合适的驱动类,但在某些情况下,你可能需要显式地指定驱动类。你可以在 `application.properties` 或 `application.yml` 中设置:

```properties

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

```

或在 `application.yml` 中:

```yaml

spring:

  datasource:

    url: jdbc:mysql://localhost:3306/yourdatabase?useSSL=false&serverTimezone=UTC&characterEncoding=utf8

    username: yourusername

    password: yourpassword

    driver-class-name: com.mysql.cj.jdbc.Driver

    hikari:

      maximum-pool-size: 10

      minimum-idle: 5

      idle-timeout: 30000

      connection-timeout: 30000

      max-lifetime: 1800000

```

### 4. **依赖版本问题**

有时,不兼容或过时的库版本也可能导致问题。确保所有依赖项,包括 Spring Boot 和 MySQL 驱动程序,都是兼容的最新版本。

### 5. **Spring Boot Starter 使用**

如果你使用 Spring Boot 的 starter 来简化配置,确保使用的是正确的 starter。例如,使用 `spring-boot-starter-data-jpa` 时,通常会自动包括适当的数据库依赖,但如果缺少 MySQL 依赖,则需要手动添加。

### 6. **多数据源配置**

如果你在项目中配置了多个数据源,确保每个数据源都有正确的配置,并且 Spring 能够明确识别哪个数据源应该使用哪个驱动。

4、springboot和mybatis-plus版本不兼容导致

ddlApplicationRunner' 的 bean 被期望是 org.springframework.boot.Runner 类型

问题和解决:

Bean named 'ddlApplicationRunner' is expected to be of type 'org.springframework.boot. Runner' but was actually of type 'org.springframework.beans.factory.support.NullBean'

at org.springframework.beans.factory.support.AbstractBeanFactory.adaptBeanInstance(AbstractBeanFactory.java:410) ~[spring-beans-6.1.3.jar:6.1.3]

Bean named 'ddlApplicationRunner' is expected to be of type 'org.springframework.boot. Runner' but was actually of type 'org.springframework.beans.factory.support.NullBean'

at org.springframework.beans.factory.support.AbstractBeanFactory.adaptBeanInstance(AbstractBeanFactory.java:410) ~[spring-beans-6.1.3.jar:6.1.3]

5、springboot项目启动的解释

问题和解决:

从日志中可以看出,你的 Spring Boot 应用程序启动时发生了一些重要事件。以下是对日志的详细解释:

### 日志解释

1. **启动信息**:

   ```plaintext

   2024-07-24T14:17:51.254+08:00  INFO 20596 --- [           main] o.l.neg_platform.NegPlatformApplication  : Starting NegPlatformApplication using Java 17.0.8 with PID 20596 (D:\WorkPace\JavaWorkpace\neg_platform\target\classes started by 86195 in D:\WorkPace\JavaWorkpace\neg_platform)

   ```

   - 应用程序 `NegPlatformApplication` 正在使用 Java 17.0.8 版本启动,进程 ID 是 20596。

   - 应用程序的运行目录在 `D:\WorkPace\JavaWorkpace\neg_platform`。

2. **没有设置活动的 Spring Profile**:

   ```plaintext

   2024-07-24T14:17:51.263+08:00  INFO 20596 --- [           main] o.l.neg_platform.NegPlatformApplication  : No active profile set, falling back to 1 default profile: "default"

   ```

   - 没有设置活动的 Spring Profile,因此使用了默认的 "default" 配置。

3. **MyBatis Mapper 未找到**:

   ```plaintext

   2024-07-24T14:17:51.892+08:00  WARN 20596 --- [           main] o.m.s.mapper.ClassPathMapperScanner      : No MyBatis mapper was found in '[org.louis.neg_platform]' package. Please check your configuration.

   ```

   - 这是一个警告信息,表示在指定的包 `[org.louis.neg_platform]` 中没有找到 MyBatis Mapper 接口。

   - 这通常意味着你可能没有正确配置 MyBatis 的扫描路径,或是你的 Mapper 接口没有放置在正确的位置,或是没有添加必要的注解,例如 `@Mapper`。

4. **Tomcat Web 服务器启动**:

   ```plaintext

   2024-07-24T14:17:52.218+08:00  INFO 20596 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)

   ```

   - 嵌入式的 Tomcat Web 服务器初始化在端口 8080 上。

5. **启动 Tomcat 服务**:

   ```plaintext

   2024-07-24T14:17:52.226+08:00  INFO 20596 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]

   2024-07-24T14:17:52.226+08:00  INFO 20596 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.26]

   ```

   - Tomcat 服务开始启动,使用的 Servlet 引擎版本是 Apache Tomcat 10.1.26。

6. **Spring 上下文初始化**:

   ```plaintext

   2024-07-24T14:17:52.267+08:00  INFO 20596 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext

   2024-07-24T14:17:52.268+08:00  INFO 20596 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 946 ms

   ```

   - Spring 的嵌入式 WebApplicationContext 初始化完成。

7. **MyBatis-Plus 版本信息**:

   ```plaintext

    _ _   |_  _ _|_. ___ _ |    _ 

   | | |\/|_)(_| | |_\  |_)||_|_\ 

        /               |         

                           3.5.7 

   ```

   - 这是 MyBatis-Plus 的启动标志,显示使用的版本是 3.5.7。

8. **应用程序启动完成**:

   ```plaintext

   2024-07-24T14:17:52.936+08:00  INFO 20596 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'

   2024-07-24T14:17:52.942+08:00  INFO 20596 --- [           main] o.l.neg_platform.NegPlatformApplication  : Started NegPlatformApplication in 2.14 seconds (process running for 2.803)

   ```

   - Tomcat 成功启动并监听端口 8080,应用程序 `NegPlatformApplication` 在大约 2.14 秒内启动完成。

### 需要关注的部分

1. **MyBatis Mapper 未找到**:

   - 这是一个需要注意的问题。你需要确保正确配置了 MyBatis Mapper 扫描路径。例如,在 `application.yml` 或 `application.properties` 中设置 `mybatis.mapper-locations`,或者在启动类上使用 `@MapperScan` 注解来指定扫描路径。

   ```java

   @SpringBootApplication

   @MapperScan("com.example.mapper")

   public class MyApplication {

       public static void main(String[] args) {

           SpringApplication.run(MyApplication.class, args);

       }

   }

   ```

确保这些问题得到解决后,应用程序才能正常运行并正确连接到数据库。

6、jdbc驱动过时

问题:

MySQL JDBC Driver 是 Java 应用程序用来与 MySQL 数据库进行交互的驱动程序。它实现了 JDBC(Java Database Connectivity)API,这是 Java 标准的一部分,用于提供数据库访问的统一接口。以下是 MySQL JDBC Driver 的几个主要作用:

1. **数据库连接**:

   JDBC Driver 允许 Java 应用程序建立到 MySQL 数据库的连接。这是数据库操作的第一步。

2. **SQL 语句执行**:

   通过 JDBC Driver,应用程序可以执行 SQL 语句,包括查询(SELECT)、更新(UPDATE)、插入(INSERT)和删除(DELETE)操作。

3. **事务管理**:

   JDBC Driver 支持事务管理,确保数据的完整性和一致性。它允许应用程序提交或回滚事务。

4. **数据检索和更新**:

   JDBC Driver 提供了从数据库检索数据和向数据库发送数据的机制。它可以使用 `ResultSet` 对象来处理查询结果。

5. **预处理语句**:

   JDBC Driver 支持预处理语句(PreparedStatement),这可以提高性能并防止 SQL 注入攻击。

6. **批量操作**:

   JDBC Driver 允许执行批量操作,这对于大量数据的插入或更新非常有用。

7. **元数据访问**:

   应用程序可以使用 JDBC Driver 访问数据库的元数据,例如表结构、列信息等。

8. **连接池支持**:

   JDBC Driver 可以与连接池(如 HikariCP、Apache DBCP 或 C3P0)集成,以提高数据库连接的效率和性能。

9. **安全性**:

   JDBC Driver 支持加密连接(如 SSL),确保数据传输的安全性。

10. **国际化和本地化**:

    JDBC Driver 支持多种字符集和语言,以适应全球化应用程序的需求。

总之,MySQL JDBC Driver 是 Java 应用程序与 MySQL 数据库进行通信的桥梁,提供了执行数据库操作所需的所有功能。

你的 `pom.xml` 中的 `mysql-connector-j` 依赖项已经指定了较新的版本 8.2.0,这是一个较新的 MySQL JDBC 驱动版本。在这个版本中,驱动类名 `com.mysql.jdbc.Driver` 已经被 `com.mysql.cj.jdbc.Driver` 替代。因此,通常不需要手动指定驱动类名,因为驱动会自动注册。

### 需要做的事情:

1. **检查驱动类**:

   - 如果你的代码中有类似 `Class.forName("com.mysql.jdbc.Driver")` 的行,你应该将其更新为 `Class.forName("com.mysql.cj.jdbc.Driver")`。但请注意,通常现代驱动不再需要手动加载驱动类,因为驱动会自动注册。

2. **更新 JDBC 连接字符串**:

   - 确保你的 JDBC 连接字符串是符合最新标准的。通常连接字符串格式如下:

     ```properties

     jdbc:mysql://hostname:port/dbname?useSSL=false&serverTimezone=UTC

     ```

   - 注意参数的正确性,例如 `useSSL` 和 `serverTimezone`,这些参数可能在不同版本中有变化。

3. **确保配置正确**:

   - 确保你没有遗留旧的驱动或配置,尽量清理掉不必要的旧依赖和设置。

### 示例 `pom.xml` 更新:

如果你已经使用了最新版本的驱动,`pom.xml` 文件中的依赖部分不需要更改。你的依赖配置看起来已经是正确的:

```xml

<dependency>

    <groupId>com.mysql</groupId>

    <artifactId>mysql-connector-j</artifactId>

    <version>8.2.0</version>

    <scope>runtime</scope>

</dependency>

```

### 总结:

- 你的 `pom.xml` 中的依赖配置已经是使用最新版本的驱动。

- 检查和更新代码中是否还有手动加载驱动类的旧代码。

- 确保你的 JDBC 连接字符串和其他相关配置是适用于新版驱动的。

通过这些步骤,你可以确保你的项目兼容性和使用的是最新的驱动特性。

7、springboot项目启动时候提示:Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

问题和解决:

大概意思就是jvm的类加载器的执行顺序,有些第三方的包加载顺序并没有添加在bootstarp classloader中,所以提示。

当在IDEA中连接Redis时出现"Java HotSpot™ 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended"错误,通常是因为类加载器(ClassLoader)的共享机制引发的警告。

Java的类加载机制涉及到Bootstrap ClassLoader、Extension ClassLoader和Application ClassLoader。Bootstrap ClassLoader负责加载核心类库,Extension ClassLoader负责加载Java扩展库,而Application ClassLoader负责加载应用程序的类。

当IDEA启动时,IDEA会改变类加载器的加载方式,在应用程序启动时将一些核心类库路径添加到BootStrap ClassLoader的搜索路径中。这样,当应用程序执行时,某些类库会先由BootStrap ClassLoader加载,然后再由Application ClassLoader加载。

然而,由于Redis客户端库通常不是放在BootStrap ClassLoader的搜索路径下的,所以在加载Redis客户端库时,会出现上述警告。

虽然出现警告,但通常不会影响应用程序的正常运行。你可以忽略该警告,或者通过设置VM选项来关闭该警告。

如果你希望关闭警告,可以尝试以下方法:

在IDEA的启动选项中,添加以下JVM参数:

-Dsun.misc.URLClassPath.disableJarChecking=true

在IDEA的启动选项中,添加以下JVM参数:

-XX:-ShareBootClassLoader

8、Druid监控页面无法打开(404)

问题解决:

配置了druid的yml和导入了依赖项,但是项目启动时候访问http://127.0.0.1:8080/druid/api.html

会页面打不开报404,主要原因在于springboot3版本和druid的版本不一致,所以导致了项目启动时候druid初始化,然后就closed了

这样配置就能访问druid的监控页面了

9、Arrays.asList()是泛型方法,传递的数组必须是对象数组,而不是基本类型。

在Java中,`Arrays.asList()`方法将一个数组转换为一个固定大小的列表(`List`)。这个方法是泛型的,意味着它可以接受任何类型的对象数组作为参数,并返回一个包含这些对象的列表。然而,它不接受基本数据类型的数组,如`int[]`、`char[]`等。

这是因为Java中的泛型只能用于对象类型,而不能用于基本数据类型。基本数据类型如`int`、`char`等不是对象,它们有各自的封装类(如`Integer`、`Character`)作为对象类型的替代。`Arrays.asList()`方法需要一个对象数组,因为它返回的列表是一个包含对象的`List`。

如果你想使用基本类型的数组,例如`int[]`,你需要将它们转换为对应的封装类对象数组,如`Integer[]`。例如:

```java

int[] intArray = {1, 2, 3};

List<Integer> list = Arrays.asList(intArray); // 这是不对的,因为intArray是基本类型数组

// 正确的做法是:

Integer[] integerArray = {1, 2, 3};

List<Integer> list = Arrays.asList(integerArray);

```

在上述代码中,`intArray`是基本类型数组,因此不能直接传递给`Arrays.asList()`。相反,`integerArray`是`Integer`对象数组,可以直接传递给`Arrays.asList()`。

如果你有一个基本类型的数组,并且希望将其转换为列表,可以使用Java 8中的`Stream` API:

```java

int[] intArray = {1, 2, 3};

List<Integer> list = Arrays.stream(intArray)

                            .boxed()

                            .collect(Collectors.toList());

```

这种方法首先将基本类型数组转换为流(stream),然后使用`boxed()`方法将其转换为封装类型,最后收集为列表。

10、for循环操作集合异常

在Java中,能否在遍历集合的过程中增加或删除元素取决于你使用的遍历方法和集合的类型:

1. **使用 `for-each` 循环**:

   使用 `for-each` 循环遍历集合时,通常不能直接在循环中增加或删除元素,因为这可能导致 `ConcurrentModificationException`。例如:

   ```java

   List<String> list = new ArrayList<>();

   for (String item : list) { -- -->

       // 尝试删除元素

       list.remove(item); // 会抛出 ConcurrentModificationException

   }

   ```

2. **使用 `Iterator`**:

   使用 `Iterator` 遍历集合时,可以直接使用 `Iterator` 的 `remove` 方法删除元素,这是安全的。例如:

   ```java

   Iterator<String> iterator = list.iterator();

   while (iterator.hasNext()) {

       String item = iterator.next();

       if (某个条件) {

           iterator.remove(); // 安全删除元素

       }

   }

   ```

3. **使用 `while` 循环和索引**:

   使用 `while` 循环和索引遍历 `List` 时,可以在循环中安全地删除元素,但需要小心地调整索引以避免 `IndexOutOfBoundsException`。例如:

   ```java

   List<String> list = new ArrayList<>();

   int i = 0;

   while (i < list.size()) {

       String item = list.get(i);

       if (某个条件) {

           list.remove(i); // 删除元素,i 不变

       } else {

           i++; // 继续遍历

       }

   }

   ```

4. **增加元素**:

   在遍历集合时增加元素通常没有限制,但会增加遍历的复杂性,因为集合的大小会变化。如果使用 `for-each` 或原始的 `Iterator`,可能需要重新设计算法以适应集合大小的变化。

5. **并发集合**:

   对于并发集合(如 `ConcurrentHashMap` 的键视图、值视图、条目集),可以在遍历过程中安全地进行增加或删除操作,因为这些集合的实现允许并发修改。

6. **`CopyOnWriteArrayList`**:

   使用 `CopyOnWriteArrayList` 时,可以在遍历过程中安全地进行增加和删除操作,因为它对修改操作使用了复制底层数组的策略。

总结来说,是否能够在遍历中增加或删除元素取决于你使用的集合类型和遍历方法。对于普通的集合和 `for-each` 循环,直接修改集合是不安全的。使用 `Iterator` 或特定类型的集合(如 `CopyOnWriteArrayList`)可以在遍历中安全地修改集合。在设计算法时,考虑到这些因素以避免运行时异常。

叶星荣 7-30 11:36:14

`java.util.ConcurrentModificationException` 异常在Java中通常在以下几种情况下抛出:

1. **使用 `for-each` 循环遍历集合**:

   当使用 `for-each` 循环遍历 `List` 集合时,如果在循环体内部直接调用 `remove` 方法或其他修改集合的方法,会抛出此异常。这是因为 `for-each` 循环背后的迭代器(Iterator)期望集合在遍历过程中不会被修改。

2. **迭代器(Iterator)的 `remove` 方法**:

   使用迭代器的 `remove` 方法删除元素是安全的,但前提是你不能在同一个迭代器上再次调用 `next` 方法之前调用 `remove`。否则,也会抛出 `ConcurrentModificationException`。

3. **并发修改**:

   当集合被一个线程修改,而另一个线程正在迭代同一个集合时,也可能会抛出这个异常。

4. **错误的使用方式**:

   如果在迭代过程中,修改了集合的结构(例如添加或删除元素),而没有通过迭代器的 `remove` 方法,就可能导致这个异常。

在你提供的堆栈跟踪中,异常发生在 `ArrayList$Itr.checkForComodification` 方法调用时,这表明迭代器检测到集合被修改了。具体来说,是因为在 `for-each` 循环中尝试删除元素,违反了迭代器的规则。

要解决这个问题,你可以:

- 使用迭代器的 `remove` 方法来安全地删除元素。

- 收集需要删除的元素的引用或索引,然后在循环结束后删除它们。

- 使用 `CopyOnWriteArrayList` 这样的并发集合,它允许在迭代时进行修改而不抛出异常。

下面是使用迭代器正确删除元素的示例:

```java

List<String> list = new ArrayList<>();

// 假设列表中有元素

list.add("Element 1");

list.add("Element 2");

list.add("Element 3");

Iterator<String> iterator = list.iterator();

while (iterator.hasNext()) {

    String element = iterator.next();

    // 条件满足时删除元素

    if ("Element 2".equals(element)) {

        iterator.remove();

    }

}

```

如果你的目的是测试这个异常是否被抛出,确保你的测试逻辑中包含了在遍历过程中修改集合的操作。如果你不想抛出异常,确保在遍历过程中不要修改集合。

11、java的值传递(数组/集合 && 对象)

在Java中,方法参数是按值传递的,即传递的是参数的副本,而不是参数的引用。因此,当你试图在`swap`方法中交换两个`Person`对象时,你只是交换了它们的副本,而不会影响到实际传入的对象。

让我们详细解释一下:

1. **按值传递**:

   - 当你调用`swap(xiaoZhang, xiaoLi)`时,Java将`xiaoZhang`和`xiaoLi`的引用(即内存地址)复制到`swap`方法的`person1`和`person2`参数中。

   - 这意味着在`swap`方法中,`person1`和`person2`是指向原始对象的引用的副本,而不是原始引用本身。

2. **在`swap`方法中的交换**:

   - 你声明了一个新的`Person`类型的局部变量`temp`,并将`person1`的值(即指向`xiaoZhang`对象的引用)赋值给`temp`。

   - 然后你将`person2`的值(即指向`xiaoLi`对象的引用)赋值给`person1`。

   - 最后,你将`temp`的值(即最初指向`xiaoZhang`对象的引用)赋值给`person2`。

这些操作只是交换了`person1`和`person2`引用的副本,而不是原始的引用。因此,当`swap`方法结束时,`xiaoZhang`和`xiaoLi`仍然指向它们原始的对象。

3. **原始对象未受影响**:

   - `swap`方法完成后,`xiaoZhang`和`xiaoLi`的引用没有改变,它们仍然指向创建时的原始对象。

   - 因此,打印`xiaoZhang.getName()`和`xiaoLi.getName()`时,仍然会输出原始的值,即"小张"和"小李"。

这是Java中按值传递的一个典型例子。要真正交换两个对象的状态,你需要在对象本身上进行操作,而不是在引用的副本上。比如,可以在`Person`类中添加一个方法来交换两个对象的属性,或者使用一个容器对象来包装这些对象,然后交换容器的引用。

那为什么传递数组和集合不是原始对象的拷贝呢?

在Java中,数组和集合的处理方式与基本类型和对象引用的处理方式有所不同。具体来说,当你传递一个数组或集合到一个方法时,传递的仍然是引用的副本。但是,这种引用指向的是数组或集合的实际对象,因此你可以通过这个引用修改数组或集合的内容。

以下是详细解释:

### 1. 数组

数组在Java中是对象。当你传递一个数组到方法时,传递的是数组对象的引用的副本。这意味着方法接收到的引用仍然指向原始数组对象。因此,如果你在方法中通过引用修改数组的内容,原始数组将被修改。

示例:

```java

public static void modifyArray(int[] arr) {

    arr[0] = 100; // 修改数组的第一个元素

}

public static void main(String[] args) {

    int[] myArray = {1, 2, 3};

    modifyArray(myArray);

    System.out.println(myArray[0]); // 输出 100

}

```

在这个例子中,`myArray`传递给`modifyArray`方法的是数组对象的引用的副本,但这个副本指向同一个数组对象。因此,修改`arr`的内容会直接影响到`myArray`的内容。

### 2. 集合

集合(如`List`, `Set`, `Map`)也是对象,工作原理类似于数组。当你将集合传递给方法时,传递的是集合对象的引用的副本,因此你可以通过这个引用修改集合的内容。

示例:

```java

import java.util.ArrayList;

import java.util.List;

public class Main {

    public static void modifyList(List<String> list) {

        list.add("New Element");

    }

    public static void main(String[] args) {

        List<String> myList = new ArrayList<>();

        myList.add("Element 1");

        modifyList(myList);

        System.out.println(myList); // 输出 [Element 1, New Element]

    }

}

```

在这个例子中,`myList`传递给`modifyList`方法时,传递的是`myList`的引用的副本,但这个副本仍然指向同一个集合对象。因此,`modifyList`方法中对集合的修改会反映在`myList`中。

### 总结

Java中所有对象(包括数组和集合)的引用都是按值传递的,这意味着方法接收的是引用的副本。然而,由于数组和集合引用指向的是可变对象,当你修改这些对象时,通过任何引用(无论是原始的还是副本)所做的修改都会影响到对象本身。相反,当你在方法中改变引用的副本(如`swap`方法中的对象引用),不会影响到原始引用。

public class Person { private String name; public Person(String name) { this.name = name; } public void setName(String name) { this.name = name; } public String getName() { return name; } } public static void changeName(Person person, String newName) { person.setName(newName); } public static void main(String[] args) { Person person = new Person("Alice"); changeName(person, "Bob"); System.out.println(person.getName()); // 输出 Bob } 为啥这个会影响,public class Person { private String name; // 省略构造函数、Getter&Setter方法 } public static void main(String[] args) { Person xiaoZhang = new Person("小张"); Person xiaoLi = new Person("小李"); swap(xiaoZhang, xiaoLi); System.out.println("xiaoZhang:" + xiaoZhang.getName()); System.out.println("xiaoLi:" + xiaoLi.getName()); } public static void swap(Person person1, Person person2) { Person temp = person1; person1 = person2; person2 = temp; System.out.println("person1:" + person1.getName()); System.out.println("person2:" + person2.getName()); }这个代码却不会影响

这两个代码示例展示了Java中对象引用的传递行为,但它们的处理方式有所不同,导致了不同的结果。让我们详细分析这两个示例。

### 示例1: `changeName` 方法

```java

public class Person {

    private String name;

    public Person(String name) {

        this.name = name;

    }

    public void setName(String name) {

        this.name = name;

    }

    public String getName() {

        return name;

    }

}

public static void changeName(Person person, String newName) {

    person.setName(newName);

}

public static void main(String[] args) {

    Person person = new Person("Alice");

    changeName(person, "Bob");

    System.out.println(person.getName()); // 输出 Bob

}

```

在这个示例中,`changeName` 方法接收一个 `Person` 对象的引用作为参数,并调用这个引用上的 `setName` 方法来修改 `Person` 对象的 `name` 属性。因为传递的是对象的引用的副本,而这个引用指向的是同一个对象,因此对 `person` 进行的修改会影响到原始的 `Person` 对象。

### 示例2: `swap` 方法

```java

public class Person {

    private String name;

    // 省略构造函数、Getter&Setter方法

}

public static void main(String[] args) {

    Person xiaoZhang = new Person("小张");

    Person xiaoLi = new Person("小李");

    swap(xiaoZhang, xiaoLi);

    System.out.println("xiaoZhang:" + xiaoZhang.getName());

    System.out.println("xiaoLi:" + xiaoLi.getName());

}

public static void swap(Person person1, Person person2) {

    Person temp = person1;

    person1 = person2;

    person2 = temp;

    System.out.println("person1:" + person1.getName());

    System.out.println("person2:" + person2.getName());

}

```

在这个示例中,`swap` 方法试图交换两个 `Person` 对象的引用。虽然传递给 `swap` 方法的也是对象引用的副本,但在方法内部交换的是这些引用的副本,而不是原始引用本身。具体过程如下:

1. `swap` 方法接收到两个 `Person` 对象的引用副本 `person1` 和 `person2`。

2. 变量 `temp` 存储 `person1` 的引用(指向 `xiaoZhang`)。

3. `person1` 被赋值为 `person2` 的引用(指向 `xiaoLi`)。

4. `person2` 被赋值为 `temp` 的引用(指向 `xiaoZhang`)。

这些操作只在方法内部交换了引用的副本,而不影响 `main` 方法中的 `xiaoZhang` 和 `xiaoLi` 变量。因此,原始的 `xiaoZhang` 和 `xiaoLi` 仍然指向创建时的 `Person` 对象,而不会因为 `swap` 方法中的操作而改变。

### 关键区别

- **`changeName` 方法**: 直接修改传递的对象的内部状态(通过调用 `setName` 方法)。由于传递的是引用,修改会影响到原始对象。

- **`swap` 方法**: 试图交换引用本身,但仅仅是交换了引用的副本,不会影响到实际传递给方法的引用。因此,`main` 方法中的 `xiaoZhang` 和 `xiaoLi` 的引用并未交换。

这就是为什么在 `changeName` 方法中,`Person` 对象的状态被改变,而在 `swap` 方法中,原始对象引用的状态保持不变的原因。

对于集合类型的数据结构(如`List`、`Set`、`Map`等),Java中的行为仍然是按值传递引用。这意味着传递给方法的集合引用是引用的副本,但这些副本指向同一个集合对象。因此,在方法中对集合内容的修改会反映到原始集合中。

不过,重要的是要注意,类似于对象的引用交换,集合的引用交换本身不会改变原始引用。让我们通过一个例子来说明这一点:

### 集合中的内容修改

当你在方法中修改集合的内容时,原始集合的内容也会改变。这是因为集合引用的副本指向同一个集合对象。

```java

import java.util.ArrayList;

import java.util.List;

public class Main {

    public static void modifyList(List<String> list) {

        list.add("New Element");

    }

    public static void main(String[] args) {

        List<String> myList = new ArrayList<>();

        myList.add("Initial");

        modifyList(myList);

        System.out.println(myList); // 输出 [Initial, New Element]

    }

}

```

在这个例子中,`modifyList` 方法接收一个 `List` 的引用,并向其中添加一个新元素。这种操作修改了集合的内容,因此在方法外部也能看到这些修改。

### 集合引用的交换

如果你尝试在方法中交换集合的引用,就像你尝试交换对象引用一样,这不会影响到原始的集合引用。

```java

import java.util.ArrayList;

import java.util.List;

public class Main {

    public static void swapLists(List<String> list1, List<String> list2) {

        List<String> temp = list1;

        list1 = list2;

        list2 = temp;

    }

    public static void main(String[] args) {

        List<String> listA = new ArrayList<>();

        listA.add("A1");

        

        List<String> listB = new ArrayList<>();

        listB.add("B1");

        

        swapLists(listA, listB);

        System.out.println("listA: " + listA); // 输出 [A1]

        System.out.println("listB: " + listB); // 输出 [B1]

    }

}

```

在这个例子中,`swapLists` 方法试图交换两个 `List` 对象的引用。虽然在方法内部,`list1` 和 `list2` 被交换了,但这只是交换了引用的副本,不会影响到原始的集合引用。因此,`listA` 和 `listB` 在方法调用后仍然指向它们原始的集合对象。

### 关键点总结

- **集合内容的修改**: 通过引用副本修改集合的内容会反映在原始集合中,因为这些引用指向同一个对象。

- **集合引用的交换**: 在方法中交换集合的引用只会影响引用的副本,不会影响到方法外的原始引用。

要真正交换两个集合对象中的数据,可以选择在方法中修改集合内容(如复制或替换元素),或者使用一个中间数据结构来交换数据。

总结:

java引用类型作为方法参数传递的时候,实际上是原始对象的拷贝,但是指向的地址是原始对象,所以对拷贝对象的修改会影响原始对象,但是对于方法中,对拷贝对象的交换,在出方法后,并不会对原始对象产生影响。


总结




声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。