JAVA 二维列表的基础操作与异常

server/2025/2/6 19:00:27/

在Java中创建二维 ArrayList(即嵌套列表)的方法有多种,下面我将详细介绍常用的几种方式,并分析它们的区别和适用场景。


1. 使用嵌套 ArrayList 创建二维列表

方法一:直接嵌套 ArrayList

这是最常用的方法,创建一个 ArrayList,每个元素本身又是一个 ArrayList,从而形成二维结构。

示例代码:

import java.util.ArrayList;public class TwoDArrayListExample {public static void main(String[] args) {// 创建二维 ArrayListArrayList<ArrayList<Integer>> twoDList = new ArrayList<>();// 初始化第一行ArrayList<Integer> row1 = new ArrayList<>();row1.add(1);row1.add(2);row1.add(3);// 初始化第二行ArrayList<Integer> row2 = new ArrayList<>();row2.add(4);row2.add(5);row2.add(6);// 添加行到二维列表twoDList.add(row1);twoDList.add(row2);// 输出二维列表System.out.println(twoDList);}
}

输出:

[[1, 2, 3], [4, 5, 6]]

优点:

  • 灵活,可以处理不规则的二维结构(每行的长度可以不同)。
  • 易于理解和实现。

缺点:

  • 需要手动管理每一行的初始化和添加,代码较繁琐。
  • 访问元素的时间复杂度稍高,因为 ArrayList 是基于动态数组实现的,扩展时可能涉及数组复制。

方法二:在循环中动态初始化

这种方法通过循环来自动生成多行多列,适用于需要预定义尺寸的二维列表。

示例代码:

import java.util.ArrayList;public class DynamicTwoDArrayList {public static void main(String[] args) {int rows = 3, cols = 4;ArrayList<ArrayList<Integer>> twoDList = new ArrayList<>();// 初始化二维 ArrayListfor (int i = 0; i < rows; i++) {ArrayList<Integer> row = new ArrayList<>();for (int j = 0; j < cols; j++) {row.add(i * cols + j);  // 填充数据}twoDList.add(row);}// 输出二维列表System.out.println(twoDList);}
}

输出:

[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]

优点:

  • 适合处理规则的矩阵结构,代码简洁。
  • 便于初始化大规模数据。

缺点:

  • 固定行列数量,灵活性不如直接嵌套 ArrayList

2. 使用 List<List<T>> 接口实现

虽然 ArrayList 是最常用的实现,但使用 List 接口定义可以增加代码的通用性和灵活性,便于将来切换到其他 List 实现(如 LinkedList)。

示例代码:

import java.util.List;
import java.util.ArrayList;public class ListInterfaceExample {public static void main(String[] args) {List<List<String>> twoDList = new ArrayList<>();List<String> row1 = new ArrayList<>();row1.add("A");row1.add("B");List<String> row2 = new ArrayList<>();row2.add("C");row2.add("D");twoDList.add(row1);twoDList.add(row2);System.out.println(twoDList);}
}

输出:

[[A, B], [C, D]]

优点:

  • 代码更加通用,便于后期维护。
  • 如果将来需要换成 LinkedList 或其他 List 实现,可以直接替换,代码无需大改。

缺点:

  • 性能和功能上与直接使用 ArrayList 差异不大,主要优势体现在代码结构上。

3. 使用 Arrays.asList() 快速初始化

Arrays.asList() 可以用于快速初始化嵌套 ArrayList,适用于静态、已知数据的二维列表。

示例代码:

import java.util.ArrayList;
import java.util.Arrays;public class AsListExample {public static void main(String[] args) {ArrayList<ArrayList<Integer>> twoDList = new ArrayList<>(Arrays.asList(new ArrayList<>(Arrays.asList(1, 2, 3)),new ArrayList<>(Arrays.asList(4, 5, 6)),new ArrayList<>(Arrays.asList(7, 8, 9))));System.out.println(twoDList);}
}

输出:

[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

优点:

  • 初始化快速,适合处理已知静态数据。
  • 代码简洁明了。

缺点:

  • 数据固定,不适合需要动态修改的场景。
  • Arrays.asList() 返回的列表大小固定,无法增加或删除元素(除非包裹在新的 ArrayList 中)。

4. 使用 Collections.nCopies() 创建固定大小的二维列表

如果需要创建一个固定大小的二维 ArrayList 并填充默认值,可以使用 Collections.nCopies()

示例代码:

import java.util.ArrayList;
import java.util.Collections;public class NCopiesExample {public static void main(String[] args) {int rows = 3, cols = 4;ArrayList<ArrayList<Integer>> twoDList = new ArrayList<>(Collections.nCopies(rows, new ArrayList<>(Collections.nCopies(cols, 0))));System.out.println(twoDList);}
}

输出:

[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

注意事项:

  • 上述代码实际上会让每一行都引用同一个 ArrayList 实例,修改一行会影响所有行。因此需要深拷贝来避免这个问题。

正确版本:

import java.util.ArrayList;
import java.util.Collections;public class CorrectNCopiesExample {public static void main(String[] args) {int rows = 3, cols = 4;ArrayList<ArrayList<Integer>> twoDList = new ArrayList<>();for (int i = 0; i < rows; i++) {twoDList.add(new ArrayList<>(Collections.nCopies(cols, 0)));}System.out.println(twoDList);}
}

方法对比总结

方法优点缺点适用场景
直接嵌套 ArrayList灵活、易于理解,适合不规则数据初始化和管理代码较繁琐小型项目或动态行列数据
循环动态初始化适合大规模数据,代码简洁行列固定,灵活性较差规则矩阵结构
使用 List<List<T>> 接口代码通用,便于维护和扩展与直接嵌套 ArrayList 差异不大需要考虑代码扩展性或可替换性的场景
Arrays.asList() 快速初始化初始化快速,代码简洁数据固定,无法动态增删元素静态数据初始化
Collections.nCopies()快速创建固定大小的二维列表需要深拷贝避免引用问题,稍复杂创建统一默认值的矩阵

推荐使用场景

  • 规则矩阵(如棋盘、表格数据):使用循环动态初始化。
  • 静态、已知数据:使用 Arrays.asList() 进行快速初始化。
  • 动态修改、不规则数据:直接嵌套 ArrayList,灵活管理行列。

将二维数组转换为二维列表的方法总结


1. 使用嵌套 for-each 循环

适用场景:适用于简单的遍历和转换,适合处理基本数据类型或对象数组。
代码示例(基本数据类型):
import java.util.ArrayList;public class ForEachExample {public static void main(String[] args) {int[][] array = {{1, 2, 3},{4, 5, 6},{7, 8, 9}};ArrayList<ArrayList<Integer>> list = new ArrayList<>();for (int[] row : array) {ArrayList<Integer> tempList = new ArrayList<>();for (int num : row) {tempList.add(num);}list.add(tempList);}System.out.println(list);  // 输出: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]}
}
注意事项:
  • 适用于规则或不规则的数组结构。
  • 不适合处理需要索引访问的场景,因为 for-each 循环不提供索引信息。

2. 使用 Stream.forEach 流式处理

适用场景:适用于大数据处理或需要链式调用的场景,代码简洁且易于扩展。
代码示例(基本数据类型):
import java.util.ArrayList;
import java.util.Arrays;public class StreamForEachExample {public static void main(String[] args) {int[][] array = {{1, 2, 3},{4, 5, 6},{7, 8, 9}};ArrayList<ArrayList<Integer>> list = new ArrayList<>();Arrays.stream(array).forEach(row -> {ArrayList<Integer> tempList = new ArrayList<>();Arrays.stream(row).forEach(tempList::add);list.add(tempList);});System.out.println(list);  // 输出: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]}
}
注意事项:
  • forEach 用于流的遍历,但不能修改流本身的结构。
  • 适合简单转换,但对于复杂链式操作建议使用 mapcollect

3. 使用 Stream.mapcollect(推荐)

适用场景:更优雅的流式处理方式,适合复杂数据转换或链式操作。
代码示例(基本数据类型):
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;public class StreamMapExample {public static void main(String[] args) {int[][] array = {{1, 2, 3},{4, 5, 6},{7, 8, 9}};List<List<Integer>> list = Arrays.stream(array).map(row -> Arrays.stream(row).boxed()  // 将 int 转换为 Integer.collect(Collectors.toList())).collect(Collectors.toList());System.out.println(list);  // 输出: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]}
}
代码示例(自定义类):
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;public class OXOGame {public static void main(String[] args) {OXOPlayer[][] players = {{new OXOPlayer("O")},{new OXOPlayer("X")}};List<List<OXOPlayer>> playerList = Arrays.stream(players).map(row -> Arrays.stream(row).collect(Collectors.toList())).collect(Collectors.toList());playerList.forEach(row -> {row.forEach(player -> System.out.print(player.getPlayingLetter() + " "));System.out.println();});}
}class OXOPlayer {private String playingLetter;public OXOPlayer(String letter) {this.playingLetter = letter;}public String getPlayingLetter() {return playingLetter;}
}
注意事项:
  • .boxed() 仅适用于基本数据类型,处理自定义类时无需使用。
  • 使用 mapcollect 可以实现链式操作,代码更简洁易读。

4. 使用 flatMap 展平为一维列表(进阶用法)

适用场景:当需要将二维数组转换为一维列表时使用。
代码示例(自定义类展平):
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;public class FlatMapExample {public static void main(String[] args) {OXOPlayer[][] players = {{new OXOPlayer("O"), new OXOPlayer("X")},{new OXOPlayer("X"), new OXOPlayer("O")}};List<OXOPlayer> flatList = Arrays.stream(players).flatMap(Arrays::stream).collect(Collectors.toList());flatList.forEach(player -> System.out.print(player.getPlayingLetter() + " "));// 输出: O X X O}
}class OXOPlayer {private String playingLetter;public OXOPlayer(String letter) {this.playingLetter = letter;}public String getPlayingLetter() {return playingLetter;}
}
注意事项:
  • flatMap 会将嵌套的流展平成单层流,适合处理需要扁平化的数据结构。
  • 展平后无法保留原有的二维结构,适合一维数据处理。

总结

方法适用场景代码简洁性是否支持链式操作备注
嵌套 for-each 循环简单遍历,适合初学者一般适用于简单数据转换,但代码较冗长
Stream.forEach简单流式处理,适合中等规模数据较简洁不支持复杂链式操作,适用于遍历但不适合结构修改
Stream.map + collect推荐方法,适合复杂数据转换和链式操作非常简洁支持高级流操作,适用于自定义类和基本数据类型
flatMap 展平处理将二维数组转换为一维列表,适合扁平化需求简洁适合需要将嵌套结构扁平化处理的场景


在 Java 中,你提到的这个情况其实是合法的,因为它使用的是嵌套类(Nested Class),而不是定义多个顶层 public class


1. 嵌套类的定义

在 Java 中,可以在一个类的内部定义另一个类,这种内部类可以是 publicprotectedprivatedefault 访问级别。内部类包括:

  1. 静态嵌套类(Static Nested Class)
  2. 非静态内部类(Inner Class)
  3. 局部内部类(Local Class)
  4. 匿名内部类(Anonymous Class)

2. 你的代码属于静态嵌套类

你的代码中的 OutsideCellRangeExceptionOXOMoveException静态嵌套类,这是合法且常见的设计模式,特别是在定义特定异常子类时。

示例代码:
public class OXOMoveException extends RuntimeException {@Serial private static final long serialVersionUID = 1;public OXOMoveException(String message) {super(message);}// 定义枚举类型,用于表示行或列的错误public enum RowOrColumn { ROW, COLUMN }// 静态嵌套类,表示单元格超出范围的异常public static class OutsideCellRangeException extends OXOMoveException {public OutsideCellRangeException(String message) {super(message);}}
}

3. 为什么可以在 public 类中定义 public 静态嵌套类?

  • 访问控制不同于顶层类:在 Java 中,一个 .java 文件中只能有一个 public顶层类,文件名必须与该类名一致。然而,嵌套类不受此限制,可以在 public 顶层类中定义多个 public 嵌套类或枚举。

  • 嵌套类的命名空间:嵌套类属于外部类的命名空间,OutsideCellRangeException 被视为 OXOMoveException.OutsideCellRangeException,而不是独立的顶层类。


4. 如何使用嵌套类

你可以直接通过外部类访问嵌套的 OutsideCellRangeException 类:

public class Main {public static void main(String[] args) {try {throw new OXOMoveException.OutsideCellRangeException("Cell is outside the valid range.");} catch (OXOMoveException e) {System.out.println(e.getMessage());}}
}

输出:

Cell is outside the valid range.

静态嵌套类的特点:不依赖外部类实例:静态嵌套类不需要外部类的实例,可以直接通过外部类的名称访问。

内部类和嵌套类的区别

类型是否需要外部类实例可使用的访问修饰符常见用途
静态嵌套类不需要public, protected, private, 默认定义工具类、异常类等
非静态内部类需要public, protected, private, 默认访问外部类实例的成员变量
局部内部类需要无(局部作用域内有效)定义在方法或代码块内,临时使用的类
匿名内部类需要简化接口或抽象类的快速实现
  • 误解:认为一个 public 类中不能有另一个 public 类。

    • 解释:在同一个 .java 文件中,确实不能有两个 public 顶层类,但嵌套类(内部类)不受此限制,可以在一个 public 类中定义多个 public 嵌套类。

总结你的前两个问题:

1. 第一个问题:异常定义的方式与 OXOGame 的兼容性
  • 你的异常定义:
    你将所有异常类定义为 OXOMoveException内部静态类static class)。例如:

    public static class InvalidBoardSizeException extends OXOMoveException {public InvalidBoardSizeException() {super("Board size is larger than 9x9 or smaller than 3x3");}
    }
    

    在调用时,需要写成:

    throw new OXOMoveException.InvalidBoardSizeException();
    
  • 出现的问题:

    • 未处理异常的报错: 当你在 OXOController 中抛出异常时,OXOGame 并没有显式的 try-catch 来捕获这个异常,导致 IDE 提示“未处理异常”。
    • 异常信息显示问题: OXOGame 中虽然捕获了 OXOMoveException,但显示的错误信息可能是异常类的全名(如 OXOMoveException$InvalidBoardSizeException),而不是你定义的具体错误消息。
  • 原因:

    • 你的异常继承自 Exception,属于 检查型异常(Checked Exception),Java 强制要求在调用的地方处理(使用 try-catch 或在方法签名中 throws)。
    • OXOGame 中默认使用 exception.toString() 输出异常,而 toString() 默认返回的是类名,而不是异常消息。

2. 第二个问题:如何在不修改 OXOGame 的前提下解决异常处理
  • 解决方法:

    • 将异常从检查型异常改为非检查型异常:OXOMoveException 改为继承 RuntimeException,使其成为 非检查型异常(Unchecked Exception),这样在 OXOGame 中调用时不需要显式处理异常,IDE 也不会报错。
      public class OXOMoveException extends RuntimeException {public OXOMoveException(String message) {super(message);}
      }
      
    • 重写 toString() 方法: 为了确保 OXOGame 捕获异常时能正确显示自定义的错误信息,重写 toString() 方法,让它返回 getMessage()
      @Override
      public String toString() {return getMessage();  // 确保打印异常时显示的是消息而不是类名
      }
      
  • 最终效果:

    • OXOGame 不需要任何修改,OXOController 中抛出的异常会被正确捕获并显示详细的错误信息。
    • 控制台输出:
      Game move exception: Board size is larger than 9x9 or smaller than 3x3
      

异常定义对项目的影响:

  1. 检查型异常 vs 非检查型异常:

    • 检查型异常(Checked Exception):

      • 继承自 Exception,必须显式处理。
      • 如果不在调用的地方加 try-catchthrows,IDE 会报错。
      • 影响: 会强制你修改调用该异常的方法或类,违背题目不修改 OXOGame 的要求。
    • 非检查型异常(Unchecked Exception):

      • 继承自 RuntimeException,不需要显式处理。
      • 即使调用的地方没有 try-catch,程序仍能正常运行,异常在运行时自动处理。
      • 影响: 可以在不修改 OXOGame 的情况下,正确抛出和捕获异常,符合题目要求。
  2. 内部类 vs 外部类异常定义:

    • 内部类异常(如 OXOMoveException.InvalidBoardSizeException):
      • 结构更紧凑,便于组织代码,但在引用时路径较长,可能导致代码可读性降低。
    • 外部类异常(如 InvalidBoardSizeException extends OXOMoveException):
      • 结构清晰,引用简便,更符合常规的异常定义习惯。

supergetMessage() 方法的解释:

  1. super(message) 的作用:

    • super(message) 是调用父类构造函数的方法。在自定义异常中,调用 super(message) 将错误信息传递给 ExceptionRuntimeException 的构造函数,保存到异常对象内部。
    • 示例:
      public InvalidBoardSizeException() {super("Board size is larger than 9x9 or smaller than 3x3");
      }
      
      这表示创建 InvalidBoardSizeException 对象时,错误消息 "Board size is larger than 9x9 or smaller than 3x3" 会被保存到异常对象中。
  2. getMessage() 的作用:

    • getMessage()Throwable 类中的方法,用于返回通过 super(message) 传入的错误信息。
    • 当你调用 exception.getMessage(),会返回你定义的错误消息。
    • 示例:
      catch (OXOMoveException e) {System.out.println(e.getMessage());  // 输出:Board size is larger than 9x9 or smaller than 3x3
      }
      
  3. 为什么需要重写 toString() 方法:

    • 默认情况下,toString() 返回的是异常的类名和内存地址,类似:
      edu.uob.OXOMoveException$InvalidBoardSizeException
      
    • 为了确保打印异常时显示自定义的错误信息,可以重写 toString() 方法,让它返回 getMessage()
      @Override
      public String toString() {return getMessage();
      }
      
    • 这样,即使调用的是 System.out.println(exception),也会输出具体的错误消息,而不是类名。

总结:

  1. 你的异常定义(内部静态类)是正确的,但因为它们是 检查型异常,Java 强制要求在调用时显式处理,导致在 OXOGame 中无法直接兼容。
  2. 将异常改为非检查型异常(继承 RuntimeException 可以绕开显式处理的要求,满足题目不修改 OXOGame 的限制。
  3. 使用 super(message) 传递错误信息,使用 getMessage() 获取错误信息,重写 toString() 确保异常信息正确显示。

http://www.ppmy.cn/server/165486.html

相关文章

15.<Spring Boot 日志>

本篇文章将记录我学习SpringBoot日志 1.日志文件的用途 2.SpringBoot日志文件的配置 3.用lombook依赖引入Slf4j注解&#xff0c;从而引入log对象。方便我们打印日志。 一、日志的作用 日志主要是为了发现问题、分析问题、定位问题。除此之外、日志还有许多其他的用途。 1.系统监…

JAVA进阶之线程

为神马有线程&#xff1f;这玩意儿在干嘛&#xff1f;&#xff1f;&#xff1f; 回答这个问题&#xff0c;就先要知道一点点计算机的工作方式。 总所周知&#xff0c;计算机有五部分&#xff1a;输入输出、计算器、存储器、控制器。而在计算机内&#xff0c;CPU、内存、I/O之…

Spring理论知识(Ⅴ)——Spring Web模块

Spring的组成 Spring由20个核心依赖组成&#xff0c;这20个核心依赖可以分为6个核心模块 Spring Web模块简介 众所周知&#xff0c;Java目前最大的一个用途就是作为Web应用的服务端&#xff08;Java Web&#xff09; Spring又是JavaEE中使用最广泛的开发框架&#xff0…

【LLM】为何DeepSeek 弃用MST却采用Rejection采样

文章目录 拒绝采样 Rejection sampling&#x1f3af;马尔可夫搜索树 &#x1f333;RFT和SFT1. RFT和SFT的区别2. 如何将RFT用于数学推理任务&#xff1f; Reference 在提升大语言模型&#xff08;LLM&#xff09;推理能力时&#xff0c;拒绝采样&#xff08;Rejection Sampling…

文本分析NLP的常用工具和特点

1&#xff09;非上下文感知型文本分析工具和特点 特性VADERTextBlob适合文本类型短文本、非正式语言&#xff08;如评论、推文&#xff09;中等长度、正式文本情感强度分析支持&#xff08;正面、负面、中性&#xff09;支持&#xff08;极行、主观性&#xff09;处理表情符号…

计算机网络笔记再战——理解几个经典的协议5——围绕IP的几个辅助协议

目录 DNS DNS查询 ARP ICMP DHCP NAT DNS 没人喜欢天天背诵&#xff0c;输入一场串IP&#xff01;我们需要一个稍微有含义一点的名称——比如说www.google.com来标记我访问的是谷歌&#xff0c;而不是一大长串的IP地址&#xff01;域名服务解析就是一个完成这样的功能的一…

【gRPC-gateway】初探grpc网关,插件安装,默认实现,go案例

grpc-gateway https://github.com/grpc-ecosystem/grpc-gateway 作用 通过反向代理的方式&#xff0c;将grpc server接口转为httpjson api 使用场景 向后兼容支持grpc不支持的语言或客户端 单纯用grpc实现的服务端代码&#xff0c;只能用grpc客户端调用&#xff0c;&#…

服务器虚拟化实战:架构、技术与最佳实践

&#x1f4dd;个人主页&#x1f339;&#xff1a;一ge科研小菜鸡-CSDN博客 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; 1. 引言 服务器虚拟化是现代 IT 基础设施的重要组成部分&#xff0c;通过虚拟化技术可以提高服务器资源利用率、降低硬件成本&am…