今天看POI
源码的时候,发现HSSFWorkbook
类型的工作簿,行数据是用TreeMap<Integer, HSSFRow>
存储的,列数据是用HSSFCell[]
数组来存的;XSSFWorkbook
类型的工作簿,行数据是用SortedMap<Integer, XSSFRow>
存储的,列数据是用TreeMap<Integer, XSSFCell>
来存的。其中数组方式的代码中有初始容量还有扩容操作(直接理解为list集合就行),也就意味着里面的数组可能存在为null的数据(单元格)。
下面我们用HSSFWorkbook
类型的工作簿来分析问题
getPhysicalNumberOfCells遍历可能导致空指针等问题
看到getPhysicalNumberOfCells
的时候,感觉好像在项目代码里看到过,就搜索了一下,发现有人使用了这个方法,然后来遍历从Row
对象里面获取Cell
单元格。而且我一年多前应该也用过这个方法,可能是报错了就改了其他方法,只是当时没有去分析为什么报错。
直接说结论,使用getPhysicalNumberOfCells
的结果来遍历,可能会导致空指针问题,下面截图是HSSFWorkbook
类型的工作簿Row
行对象获取单元格物理数量的代码。
假设有一个excel
,三个单元格位置如下图所示
在Row
对象里面的cells数组中,应该是[1, 1, null, null, null, 1, null, null,,,,]
的分布(这里不够严谨,实际数组里面的是单元格对象,还有最后为null
的是因为有初始化容量和扩容机制)。getPhysicalNumberOfCells
拿到的单元格数量就是3,所以当用fori
遍历的时候,一旦列单元格不是连续的是可能会导致空指针的。我们这NPE因为我们第三个单元格是空的。
用一段代码演示一下:
public static void main(String[] args) {// 创建一个工作簿 这里就不管关闭操作final Workbook workbook = new HSSFWorkbook();// 创建一个sheetfinal Sheet sheet = workbook.createSheet();// 创建第一行对象final Row firstRow = sheet.createRow(0);// 创建三个单元格,并设置值final Cell cell_zero = firstRow.createCell(0);final Cell cell_one = firstRow.createCell(1);final Cell cell_five = firstRow.createCell(5);cell_zero.setCellValue("1");cell_one.setCellValue("1");cell_five.setCellValue("1");// 这里拿到的单元格数量是3final int physicalNumberOfCells = firstRow.getPhysicalNumberOfCells();System.out.println("单元格物理数量:" + physicalNumberOfCells);// 遍历的时候,i = 2就会发生NPE,因为firstRow.getCell(2) = nullfor (int i = 0; i < physicalNumberOfCells; i++) {System.out.println(i + ": " + firstRow.getCell(i).getStringCellValue());}}
运行截图:
如果没有报错,那就恰好你那张表指定行里面的列单元格都是连续的,中间没有为空的单元格。虽然这种情况不会报错,但是建议不要使用,如果非要使用,最好判断一下获取的单元格是否为null
。
除了NPE
问题,还有可能会出现单元格没被遍历到问题,比如我们加了判断操作,为空就跳过本次循环,最后会发现,F列的单元格是没有被遍历到的。
第一种方式,使用迭代器获取所有单元格
Row
对象提供了一个cellIterator
方法,通过这个方法,可以拿到一个包含当前行所有单元格对象的迭代器。这种方式是最推荐的。
public static void main(String[] args) {// 创建一个工作簿 这里就不管关闭操作final Workbook workbook = new HSSFWorkbook();// 创建一个sheetfinal Sheet sheet = workbook.createSheet();// 创建第一行对象final Row firstRow = sheet.createRow(0);// 创建三个单元格,并设置值final Cell cell_zero = firstRow.createCell(0);final Cell cell_one = firstRow.createCell(1);final Cell cell_five = firstRow.createCell(5);cell_zero.setCellValue("1");cell_one.setCellValue("1");cell_five.setCellValue("1");// 拿到单元格迭代器Iterator<Cell> cellIterator = firstRow.cellIterator();// 遍历每个单元格,cell一定不为nullcellIterator.forEachRemaining(cell -> {System.out.println(cell.getColumnIndex() + ": " + cell.getStringCellValue());});}
正常输出,不会有NPE问题
第二种方式,获取起始列和最后一列然后遍历
通过Row
对象的getFirstCellNum
和getLastCellNum
方法,就能拿到这行第一个单元格的下标,还有最后一个单元格后一个单元格的下标。所以遍历的时候,一定要注意getLastCellNum拿到的值,还有因为中间可能存在空的单元格,所以也要判断拿到的单元格是否为null
。虽然这种方式也可行,但是还是推荐第一种迭代器的方式。
public static void main(String[] args) {// 创建一个工作簿 这里就不管关闭操作final Workbook workbook = new HSSFWorkbook();// 创建一个sheetfinal Sheet sheet = workbook.createSheet();// 创建第一行对象final Row firstRow = sheet.createRow(0);// 创建三个单元格,并设置值final Cell cell_zero = firstRow.createCell(0);final Cell cell_one = firstRow.createCell(1);final Cell cell_five = firstRow.createCell(5);cell_zero.setCellValue("1");cell_one.setCellValue("1");cell_five.setCellValue("1");// 拿到起始的列索引,比如我们0列就有数据,那就是1short firstCellNum = firstRow.getFirstCellNum();// 拿到最后的列索引,这个要注意,比如我们最后一列下表是5,那这个拿到的就是6short lastCellNum = firstRow.getLastCellNum();System.out.println("第一单元格index:" + firstCellNum);System.out.println("最后一各单元格后一格index:" + lastCellNum);// 注意是 i < lastCellNumfor (int i = firstCellNum; i < lastCellNum; i++) {// getCell也要判断是否为null,不然也有可能出现NPE问题System.out.println(i + " :" + firstRow.getCell(i));}}
运行截图: