当前位置: 首页 > article >正文

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

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


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

方法一:直接嵌套 ArrayList

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

示例代码:

import java.util.ArrayList;

public class TwoDArrayListExample {
    public static void main(String[] args) {
        // 创建二维 ArrayList
        ArrayList<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<>();

        // 初始化二维 ArrayList
        for (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.kler.cn/a/533926.html

相关文章:

  • JavaScript前后端交互-AJAX/fetch
  • 107,【7】buuctf web [CISCN2019 华北赛区 Day2 Web1]Hack World
  • Hugging Face GGUF 模型可视化
  • DIY Shell:探秘进程构建与命令解析的核心原理
  • unity学习26:用Input接口去监测: 鼠标,键盘,虚拟轴,虚拟按键
  • PAT甲级1052、Linked LIst Sorting
  • python实现多路视频,多窗口播放功能
  • LeetCode:647.回文子串
  • java进阶专栏的学习指南
  • HTML5 教程之标签(3)
  • 2025春招,深度思考MyBatis面试题
  • 修剪二叉搜索树(力扣669)
  • 2025最新软件测试面试大全
  • 实现一个 LRU 风格的缓存类
  • java进阶1——JVM
  • 通过C/C++编程语言实现“数据结构”课程中的链表
  • Leetcode—159. 至多包含两个不同字符的最长子串【中等】Plus
  • ip属地是手机号还是手机位置?一文理清
  • 车载以太网__传输层
  • SpringBoot+SpringDataJPA项目中使用EntityManager执行复杂SQL
  • RabbitMQ中的过期时间
  • OCT图像缺陷检测
  • SpringUI Web高端动态交互元件库
  • Django 多数据库
  • vue3 + ElementPlus 封装列表表格组件包含分页
  • AllData数据中台核心菜单十二:数据同步平台