第一节 数组

Java 数组

数组对于每一门编程语言来说都是重要的数据结构之一,当然不同语言对数组的实现及处理也不尽相同。

Java 语言中提供的数组是用来存储固定大小的同类型元素。

你可以声明一个数组变量,如 numbers[100] 来代替直接声明 100 个独立变量 number0,number1,....,number99。

数组是储存在堆上的对象,可以保存多个同类型变量。

声明数组变量

首先必须声明数组变量,才能在程序中使用数组。下面是声明数组变量的语法:

dataType[] arrayRefVar;   // 首选的方法
或
dataType arrayRefVar[];  // 效果相同,但不是首选方法

注意: 建议使用 dataType[] arrayRefVar 的声明风格声明数组变量。 dataType arrayRefVar[] 风格是来自 C/C++ 语言 ,在Java中采用是为了让 C/C++ 程序员能够快速理解java语言。

创建数组

Java语言使用new操作符来创建数组,语法如下:

arrayRefVar = new dataType[arraySize];

上面的语法语句做了两件事:

  • 一、使用 dataType[arraySize] 创建了一个数组。
  • 二、把新创建的数组的引用赋值给变量 arrayRefVar。

数组变量的声明,和创建数组可以用一条语句完成,如下所示:

dataType[] arrayRefVar = new dataType[arraySize];

另外,你还可以使用如下的方式创建数组。

dataType[] arrayRefVar = {value0, value1, ..., valuek};

数组的元素是通过索引访问的。数组索引从 0 开始,所以索引值从 0 到 arrayRefVar.length-1。

下面的语句首先声明了一个数组变量 myList,接着创建了一个包含 10 个 double 类型元素的数组,并且把它的引用赋值给 myList 变量。

public class TestArray {
   public static void main(String[] args) {
      // 数组大小
      int size = 10;
      // 定义数组
      double[] myList = new double[size];
      myList[0] = 5.6;
      myList[1] = 4.5;
      myList[2] = 3.3;
      myList[3] = 13.2;
      myList[4] = 4.0;
      myList[5] = 34.33;
      myList[6] = 34.0;
      myList[7] = 45.45;
      myList[8] = 99.993;
      myList[9] = 11123;
      // myList[10] = 111;   // 这里就会出现溢出错误,是错误的写法
      // 计算所有元素的总和
      double total = 0;
      for (int i = 0; i < size; i++) {  // 关于for循环语句再第四章会详细说明
         total += myList[i];
      }
      System.out.println("总和为: " + total); // 总和为: 11367.373
   }
}

下面的图片描绘了数组 myList。这里 myList 数组里有 10 个 double 元素,它的下标从 0 到 9。

【添油加醋的Java基础】第五章 数组与字符串_夏明亮

处理数组

数组的元素类型和数组的大小都是确定的,所以当处理数组元素时候,我们通常使用基本循环或者 For-Each 循环。

示例

该实例完整地展示了如何创建、初始化和操纵数组:

public class TestArray {
   public static void main(String[] args) {
      double[] myList = {1.9, 2.9, 3.4, 3.5};  // 声明+创建+初始化
 
      // 打印所有数组元素
      for (int i = 0; i < myList.length; i++) {
         System.out.println(myList[i] + " ");
      }
      // 计算所有元素的总和
      double total = 0;
      for (int i = 0; i < myList.length; i++) {
         total += myList[i];
      }
      System.out.println("Total is " + total);
      // 查找最大元素
      double max = myList[0];
      for (int i = 1; i < myList.length; i++) {
         if (myList[i] > max) max = myList[i];
      }
      System.out.println("Max is " + max);
   }
}
For-Each 循环

JDK 1.5 引进了一种新的循环类型,被称为 For-Each 循环或者加强型循环,它能在不使用下标的情况下遍历数组。

语法格式如下:

for(type element: array)
{
    System.out.println(element);
}
实例

该实例用来显示数组 myList 中的所有元素:

public class TestArray {
   public static void main(String[] args) {
      double[] myList = {1.9, 2.9, 3.4, 3.5};
 
      // 打印所有数组元素
      for (double element: myList) {
         System.out.println(element);
      }
   }
}
数组作为函数的参数

数组可以作为参数传递给方法。

例如,下面的例子就是一个打印 int 数组中元素的方法:

public static void printArray(int[] array) {
  for (int i = 0; i < array.length; i++) {
    System.out.print(array[i] + " ");
  }
}

调用:

printArray(new int[]{3, 1, 2, 6, 4, 2});
数组作为函数的返回值
public static int[] reverse(int[] list) { // 倒序
  int[] result = new int[list.length];
 
  for (int i = 0, j = result.length - 1; i < list.length; i++, j--) {
    result[j] = list[i];
  }
  return result;
}

以上实例中 result 数组作为函数的返回值。

多维数组

多维数组可以看成是数组的数组,比如二维数组就是一个特殊的一维数组,其每一个元素都是一个一维数组,例如:

String[][] str = new String[3][4];
多维数组的动态初始化(以二维数组为例)
  1. 直接为每一维分配空间,格式如下:
type[][] varName = new type[Length1][Length2];

type 可以为基本数据类型和复合数据类型,Length1 和 Length2 必须为正整数,Length1 为行数,Length2 为列数。

例如:

int[][] a = new int[2][3];

解析:

二维数组 a 可以看成一个两行三列的数组。

  1. 从最高维开始,分别为每一维分配空间,例如:
String[][] s = new String[2][];
s[0] = new String[2];
s[1] = new String[3];
s[0][0] = new String("Good");
s[0][1] = new String("Luck");
s[1][0] = new String("to");
s[1][1] = new String("you");
s[1][2] = new String("!");

解析:

s[0]=new String[2]s[1]=new String[3] 是为最高维分配引用空间,也就是为最高维限制其能保存数据的最长的长度,然后再为其每个数组元素单独分配空间 s0=new String("Good") 等操作。

多维数组的引用(以二维数组为例)

对二维数组中的每个元素,引用方式为 arrayNameindex1,例如:

num[1][0];
Arrays 类

java.util.Arrays 类能方便地操作数组,它提供的所有方法都是静态的。

具有以下功能:

  • 给数组赋值:通过 fill 方法。
  • 对数组排序:通过 sort 方法,按升序。
  • 比较数组:通过 equals 方法比较数组中元素值是否相等。
  • 查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。

具体说明请查看下表:

序号

方法和说明

1

public static int binarySearch(Object[] a, Object key) 用二分查找算法在给定数组中搜索给定值的对象(Byte,Int,double等)。数组在调用前必须排序好的。如果查找值包含在数组中,则返回搜索键的索引;否则返回 (-(插入点) - 1)。

2

public static boolean equals(long[] a, long[] a2) 如果两个指定的 long 型数组彼此相等,则返回 true。如果两个数组包含相同数量的元素,并且两个数组中的所有相应元素对都是相等的,则认为这两个数组是相等的。换句话说,如果两个数组以相同顺序包含相同的元素,则两个数组是相等的。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。

3

public static void fill(int[] a, int val) 将指定的 int 值分配给指定 int 型数组指定范围中的每个元素。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。

4

public static void sort(Object[] a) 对指定对象数组根据其元素的自然顺序进行升序排列。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。

Java 枚举

Java 5.0引入了枚举,枚举限制变量只能是预先设定好的值。使用枚举可以减少代码中的 bug。

Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一年的 12 个月份,一个星期的 7 天,方向有东南西北等。

Java 枚举类使用 enum 关键字来定义,各个常量使用逗号 , 来分割。

enum 定义的枚举类默认继承了 java.lang.Enum 类。 并实现了 java.lang.Serializablejava.lang.Comparable 两个接口。

例如,我们为果汁店设计一个程序,它将限制果汁为小杯、中杯、大杯。这就意味着它不允许顾客点除了这三种尺寸外的果汁。

单独的类使用枚举

实例

package com.xml.a;

public class Test51 {
	public static void main(String[] args){
	      FreshJuice juice = new FreshJuice();
	      juice.size = FreshJuice.FreshJuiceSize.MEDIUM;
	      System.out.println(juice);
	      System.out.println(juice.size);
	   }
}

class FreshJuice {
	   enum FreshJuiceSize{ SMALL, MEDIUM , LARGE }
	   FreshJuiceSize size;
}

**注意:**枚举可以单独声明或者声明在类里面。方法、变量、构造函数也可以在枚举中定义。

执行结果:

com.xml.a.FreshJuice@1c4af82c
MEDIUM
内部类中使用枚举
package com.xml.a;

public class Test52 {
	enum Color
    {
        RED, GREEN, BLUE;
    }
 
    // 执行输出结果
    public static void main(String[] args)
    {
        Color c1 = Color.RED;
        System.out.println(c1);
    }
}

执行结果:

RED
迭代枚举元素
package com.xml.a;

public class Test53 {
	public static void main(String[] args) {
	    for (Color myVar : Color.values()) {
	      System.out.println(myVar);
	    }
	  }
}

enum Color
{
    RED, GREEN, BLUE;
}

执行结果:

RED
GREEN
BLUE

在其他流程控制语句中也是类似地用法。

枚举类成员

枚举跟普通类一样可以用自己的变量、方法和构造函数,构造函数只能使用 private 访问修饰符,所以外部无法调用。

枚举既可以包含具体方法,也可以包含抽象方法。 如果枚举类具有抽象方法,则枚举类的每个实例都必须实现它。

package com.xml.a;

public class Test54 {
	public static void main(String[] args) 
    { 
        MyColor c1 = MyColor.RED; // 这里会执行MyColor的构造函数;然后实例化对象c1
        System.out.println(c1);  // RED
        c1.colorInfo();  // 执行MyColor的colorInfo()方法
    } 
}

enum MyColor 
{ 
    RED, GREEN, BLUE; 
  
    // 构造函数
    private MyColor() 
    { 
        System.out.println("Constructor called for : " + this.toString()); 
    } 
  
    public void colorInfo() 
    { 
        System.out.println("Universal Color"); 
    } 
}

执行结果:

Constructor called for : RED
Constructor called for : GREEN
Constructor called for : BLUE
RED
Universal Color

第二节 字符串类型

在Java中,String类是一个非常重要且广泛使用的类,用于表示不可变的字符序列。

由于String不可变,当进行大量字符串操作时,会产生大量临时对象,影响性能。因此,Java提供了StringBuilderStringBuffer类用于高效地进行字符串操作。

String类的特点

  • 不可变性String对象一旦创建,内容无法改变。任何对字符串的修改都会产生一个新的字符串对象。
  • 常量池:为了优化内存使用和性能,Java使用字符串常量池。当创建一个字符串字面量时,如果该字符串已存在于常量池中,则会引用已有的字符串,而不是创建新的对象。

创建字符串

通过字面量

String str1 = "Hello, World!";

通过字面量创建字符串时,会在字符串常量池中查找是否已有相同内容的字符串。如果有,则引用已有字符串;如果没有,则创建新字符串并放入常量池中。

package com.xml.a;

public class Test61 {
	public static void main(String[] args) {
		String str1 = "hello";
		String str2 = "hello";
		String str3 = "Hello";
		System.out.println(System.identityHashCode(str1)); // Java中没有指针的概念因此可以使用这种方法近似地获取对象地址
		System.out.println(System.identityHashCode(str2));
		System.out.println(System.identityHashCode(str3));
		// 通过字面量创建的String变量 当他们字面值相同时,他们实际上引用内存相同位置
		
		int i1 = 10;
		int i2 = 10;
		System.out.println(System.identityHashCode(i1));
		System.out.println(System.identityHashCode(i2)); // 与System.identityHashCode(i1)也相同
	}
}

执行结果:

1651191114
1651191114
1586600255
474675244
474675244

结果中值跟运行环境直接相关,每个人执行的结果可能都不会相同;但是规律是前两个值一致,第三各与前两个不同。

通过new关键字

package com.xml.a;

public class Test61 {
	public static void main(String[] args) {
		// 字面量创建
		String str1 = "hello";
		String str2 = "hello";
		String str3 = "Hello";
		System.out.println(System.identityHashCode(str1)); // Java中没有指针的概念因此可以使用这种方法近似地获取对象地址
		System.out.println(System.identityHashCode(str2));
		System.out.println(System.identityHashCode(str3));
		// 通过字面量创建的String变量 当他们字面值相同时,他们实际上引用内存相同位置
		
		System.out.println("----------");
		int i1 = 10;
		int i2 = 10;
		System.out.println(System.identityHashCode(i1));
		System.out.println(System.identityHashCode(i2)); // 与System.identityHashCode(i1)也相同
		
		// new关键字创建
		System.out.println("----------");
		String str4 = new String("hello");
		String str5 = new String("hello");
		String str6 = new String("Hello");
		System.out.println(System.identityHashCode(str4));
		System.out.println(System.identityHashCode(str5));// 不相同
		System.out.println(System.identityHashCode(str6));// 不相同
	}
}

执行结果:

1651191114
1651191114
1586600255
----------
474675244
474675244
----------
932583850
212628335
1579572132

常用方法

获取字符串长度
int length = str1.length();
字符串拼接
String str3 = str1 + " How are you?";
String str4 = str1.concat(" How are you?");
字符串比较
boolean equals = str1.equals(str2);
boolean equalsIgnoreCase = str1.equalsIgnoreCase("hello, world!");
查找字符或子字符串
char charAt = str1.charAt(1);           // 获取指定位置的字符
int indexOf = str1.indexOf('o');        // 查找字符第一次出现的位置
int lastIndexOf = str1.lastIndexOf('o'); // 查找字符最后一次出现的位置
子字符串
String substr = str1.substring(0, 5);
替换
String replacedStr = str1.replace('o', 'a');
分割
String[] splitStr = str1.split(", ");
转换大小写
String upperCase = str1.toUpperCase();
String lowerCase = str1.toLowerCase();

字符串的不变性

字符串的不变性是指String对象一旦创建,其内容无法修改。例如:

package com.xml.a;

public class Test62 {
	public static void main(String[] args) {
		String str1 = "hello";
		System.out.println(System.identityHashCode(str1)); // 1651191114
		str1 = "world";
		System.out.println(System.identityHashCode(str1)); // 1586600255
		String str2 = "hello";
		System.out.println(System.identityHashCode(str2)); // 1651191114
        // str1最初引用字符串"hello",然后引用字符串"world",但"hello"字符串本身没有改变,只是str1的引用改变了。
	}
}

StringBuilderStringBuffer

由于String不可变,当进行大量字符串操作时,会产生大量临时对象,影响性能。因此,Java提供了StringBuilderStringBuffer类用于高效地进行字符串操作。

  • StringBuilder:线程不安全,但速度快,适用于单线程环境。
  • StringBuffer:线程安全,适用于多线程环境。
package com.xml.a;

public class Test63 {
	public static void main(String[] args) {
		StringBuilder str1 = new StringBuilder("Hello");
		System.out.println(System.identityHashCode(str1));   // 1651191114
		str1.append(", World!");
		System.out.println(System.identityHashCode(str1));   // 1651191114
		System.out.println(str1.toString());  // Hello, World!
		
		StringBuffer str2 = new StringBuffer("Hello");
		System.out.println(System.identityHashCode(str2)); // 474675244
		str2.append(", World!");
		System.out.println(System.identityHashCode(str2)); // 474675244
		System.out.println(str2.toString());  // Hello, World!
	}
}

一个综合的例子:

package com.xml.a;

public class Test64 {
	public static void main(String[] args) {
		// 字符串创建
        String str1 = "Hello, World!";
        String str2 = new String("Hello, World!");

        // 比较字符串
        System.out.println("str1.equals(str2): " + str1.equals(str2));  // true
        System.out.println("str1 == str2: " + (str1 == str2));  // false

        // 获取字符串长度
        System.out.println("str1.length(): " + str1.length()); // 13

        // 字符串拼接
        String str3 = str1 + " How are you?";
        String str4 = str1.concat(" How are you?");
        System.out.println("str3: " + str3);  // Hello, World! How are you?
        System.out.println("str4: " + str4);  // Hello, World! How are you?

        // 查找字符或子字符串
        System.out.println("str1.charAt(1): " + str1.charAt(1)); // e; 从0开始
        System.out.println("str1.indexOf('o'): " + str1.indexOf('o')); // 4; 从0开始
        System.out.println("str1.lastIndexOf('o'): " + str1.lastIndexOf('o')); // 8

        // 子字符串
        System.out.println("str1.substring(0, 5): " + str1.substring(0, 5)); // Hello

        // 替换
        System.out.println("str1.replace('o', 'a'): " + str1.replace('o', 'a')); // Hella, Warld!

        // 分割
        String[] splitStr = str1.split(", ");
        for (String s : splitStr) {
            System.out.println("Split part: " + s);
        }
        /*
         	Split part: Hello
			Split part: World!
         */

        // 转换大小写
        System.out.println("str1.toUpperCase(): " + str1.toUpperCase());  // HELLO, WORLD!
        System.out.println("str1.toLowerCase(): " + str1.toLowerCase());  // hello, world!

        // 使用StringBuilder
        StringBuilder sb = new StringBuilder("Hello");
        sb.append(", World!");
        System.out.println("StringBuilder: " + sb.toString()); // Hello, World!

        // 使用StringBuffer
        StringBuffer sbf = new StringBuffer("Hello");
        sbf.append(", World!");
        System.out.println("StringBuffer: " + sbf.toString());  // Hello, World!
	}
}

执行结果:

str1.equals(str2): true
str1 == str2: false
str1.length(): 13
str3: Hello, World! How are you?
str4: Hello, World! How are you?
str1.charAt(1): e
str1.indexOf('o'): 4
str1.lastIndexOf('o'): 8
str1.substring(0, 5): Hello
str1.replace('o', 'a'): Hella, Warld!
Split part: Hello
Split part: World!
str1.toUpperCase(): HELLO, WORLD!
str1.toLowerCase(): hello, world!
StringBuilder: Hello, World!
StringBuffer: Hello, World!

String类是Java中一个不可变的字符序列,通过字面量和new关键字创建。其不可变性保证了字符串的线程安全和性能优化。在大量字符串操作时,可以使用StringBuilderStringBuffer来提高效率。String类提供了丰富的方法用于字符串操作,使得开发者可以方便地进行各种字符串处理。

第三节 Vector类

Java的Vector类是一个动态数组,属于Java集合框架的一部分。与ArrayList类似,但Vector是线程安全的。这意味着所有方法在执行时都是同步的,因此可以在多线程环境中安全使用。

Vector类的特点

  • 动态数组:可以自动调整其大小,以容纳添加的元素。
  • 线程安全:所有方法都是同步的,可以在多线程环境中使用。
  • 初始容量和增量:可以设置初始容量和增量,以减少动态扩展的频率和开销。

创建Vector

// 使用默认的初始容量10创建一个空的Vector;Vector支持存放不同类型的是数据,但这是不推荐的
Vector vector = new Vector();
vector.add("String1");
vector.add(100); // No type checking, potential runtime error
vector.add(true);


// 使用默认的初始容量10创建一个空的Vector;推荐使用泛型generic使得Vector存放统一类型的数据
Vector<Integer> vector = new Vector<>();

// 指定初始容量创建Vector
Vector<Integer> vectorWithCapacity = new Vector<>(20);

// 指定初始容量和增量创建Vector
Vector<Integer> vectorWithCapacityAndIncrement = new Vector<>(20, 5);

// 通过集合创建Vector
List<Integer> list = Arrays.asList(1, 2, 3);
Vector<Integer> vectorFromCollection = new Vector<>(list);

常用方法

添加元素
vector.add(1);  // 添加元素到Vector的末尾
vector.addElement(2);  // 添加元素到Vector的末尾,等价于add方法
vector.add(1, 3);  // 在指定位置添加元素
获取元素
int firstElement = vector.firstElement();  // 获取第一个元素
int lastElement = vector.lastElement();  // 获取最后一个元素
int elementAtIndex = vector.get(1);  // 获取指定位置的元素;从0起
修改元素
vector.set(1, 4);  // 设定位置1的元素为4;从0起
删除元素
vector.remove(1);  // 删除位置1的元素
vector.removeElement(2);  // 删除第一次出现的元素2
vector.removeElementAt(0);  // 删除位置0的元素
vector.clear();  // 清空Vector
大小和容量
int size = vector.size();  // 获取当前元素的数量
int capacity = vector.capacity();  // 获取当前的容量
遍历
// 使用增强for循环遍历
for (int element : vector) {
    System.out.println(element);
}

// 使用迭代器遍历
Iterator<Integer> iterator = vector.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}
一个综合的例子
package com.xml.learn;

import java.util.Vector;

public class Test01 {
	public static void main(String[] args) {
		Vector<Integer> v1 = new Vector<>();  // 创建帮初始化一个默认大小的(10)存放Integer类型数据的Vector
		//添加原始
		v1.add(10); // 添加元素10到Vector末尾
		v1.addElement(20); // 添加元素20到Vector末尾
		v1.add(1, 9);  // 添加元素9到第1位; 从0起
		
		// 遍历Vector
		for(int i : v1) {
			System.out.println(i);  // 10 9 20
		}
		
		// 大小和容量
		int size = v1.size();  // 获取当前元素的数量
		int capacity = v1.capacity();  // 获取当前的容量
		System.out.println("Vector:v1的元素个数:" + size);
		System.out.println("Vector:v1当前容量:" + capacity);
		
	}
}

线程安全性

这里涉及到多线程的概念,代码看不懂没关系,等看完第10章有关多线程的知识后再回来。

Vector类的所有方法都是同步的,因此在多线程环境中使用时是线程安全的。以下是一个多线程环境中使用Vector的示例:

package com.xml.learn;

import java.util.Vector;

public class Test02 {
	public static void main(String[] args) throws InterruptedException {
		Vector<Integer> v1 = new Vector<>();
		Runnable task = ()->{  // 线程体
			for(int i=0; i < 5; i++) {
				v1.add(i+1);   // 在v1末尾插入数据i+1
				try {
					Thread.sleep(100);  // 当前线程休眠0.1s; 为了体现线程间并行强行使进程的执行速度变慢
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		};
		
		Thread t1 = new Thread(task);  // 创建线程;使线程处于就绪状态
		Thread t2 = new Thread(task);  // 创建线程;使线程处于就绪状态
		
		t1.start(); // 执行;实际上使开始排队
		t2.start(); // 执行;实际上使开始排队
		
		t1.join();  // 如果先执行完,则等其他线程介绍;若自己是最后一个,则继续下一步
		t2.join();  // 如果先执行完,则等其他线程介绍;若自己是最后一个,则继续下一步
		
		System.out.println("v1的大小是:" + v1.size());
		// 遍历v1
		for(int i : v1) {
			System.out.println(i);  // 实际结果应该是两个线程轮流交替插入数据;若出现t1先完成,再开始t2的情况则可能是由于线程执行太快,t1在t2排队前就完成了作业
		}
	}
}

我的执行结果:

v1的大小是:10
1
1
2
2
3
3
4
4
5
5

ArrayList的比较

线程安全性Vector是线程安全的,而ArrayList不是。如果需要在多线程环境中使用ArrayList,需要手动进行同步。

性能:由于Vector的所有方法都是同步的,因此在单线程环境中,Vector的性能比ArrayList低。

扩容方式Vector在扩容时容量增加一倍,而ArrayList扩容时增加50%。

第四节 ArrayList类

ArrayList是Java集合框架中的一个重要类,用于动态地存储和管理元素。它是一个基于数组实现的可变大小的列表,提供了许多方便的方法来操作元素。

ArrayList类的特点

  • 动态数组ArrayList可以自动调整其大小,以容纳添加的元素。
  • 非同步ArrayList不是线程安全的,如果在多线程环境中使用,需手动进行同步。
  • 随机访问:由于底层是数组实现,因此支持快速的随机访问。

创建ArrayList

// 使用默认的初始容量10创建一个空的ArrayList
ArrayList<Integer> arrayList = new ArrayList<>();

// 指定初始容量创建ArrayList
ArrayList<Integer> arrayListWithCapacity = new ArrayList<>(20);

// 通过集合创建ArrayList
List<Integer> list = Arrays.asList(1, 2, 3);
ArrayList<Integer> arrayListFromCollection = new ArrayList<>(list);

常用方法

添加元素
arrayList.add(1);  // 添加元素到ArrayList的末尾
arrayList.add(1, 2);  // 在指定位置添加元素;从0开始
arrayList.addAll(Arrays.asList(3, 4, 5));  // 添加一个集合中的所有元素
获取元素
int elementAtIndex = arrayList.get(1);  // 获取指定位置的元素
修改元素
arrayList.set(1, 3);  // 修改指定位置的元素
删除元素
arrayList.remove(1);  // 删除指定位置的元素;从0开始
arrayList.remove(Integer.valueOf(2));  // 删除第一次出现的元素2
arrayList.removeAll(Arrays.asList(3, 4));  // 删除集合中的所有元素3和4
arrayList.clear();  // 清空ArrayList
大小和容量
int size = arrayList.size();  // 获取当前元素的数量
// 容量把那个没有给出直接可用的方法
检查元素
boolean contains = arrayList.contains(2);  // 检查ArrayList中是否包含指定元素
int index = arrayList.indexOf(2);  // 获取指定元素的索引
遍历
// 使用增强for循环遍历
for (int element : arrayList) {
    System.out.println(element);
}

// 使用迭代器遍历
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}
一个综合的例子
package com.xml.learn;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;

public class Test03 {
	public static void main(String[] args) {
		// ArrayList可以存储不同类型的数据,但这不是推荐的做法
		ArrayList al01 = new ArrayList();
		al01.add('x');
		al01.add(0, 20);
		al01.add("Hello");
		System.out.println("al01的大小:" + al01.size());
		for(int i=0; i<al01.size(); i++) {
			System.out.println(al01.get(i));
		}
		
		
		System.out.println("----------");
		// 创建ArrayList
        ArrayList<Integer> arrayList = new ArrayList<>();

        // 添加元素
        arrayList.add(1);  // {1}
        arrayList.add(2);  // {1,2}
        arrayList.add(1, 3); // {1,3,2}
        arrayList.addAll(Arrays.asList(4, 5, 6));  // {1,3,2,4,5,6}

        // 获取元素
        System.out.println("Element at index 1: " + arrayList.get(1));  // 3

        // 修改元素
        arrayList.set(1, 7);  // {1,7,2,4,5,6}
        System.out.println("Modified element at index 1: " + arrayList.get(1)); // 7

        // 删除元素
        arrayList.remove(1);  // {1,2,4,5,6}
        arrayList.remove(Integer.valueOf(2)); // {1,4,5,6}
        arrayList.removeAll(Arrays.asList(4, 5)); // {1,6}

        // 遍历ArrayList
        for (int element : arrayList) {
            System.out.println("Element: " + element);  // 1  6
        }

        // 使用迭代器遍历
        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            System.out.println("Iterator element: " + iterator.next());  // 1  6
        }

        // 大小和容量
        System.out.println("ArrayList size: " + arrayList.size());  // 2

        // 检查元素
        System.out.println("ArrayList contains 3: " + arrayList.contains(3));  // false
        System.out.println("Index of 3: " + arrayList.indexOf(3)); // -1

        // 清空ArrayList
        arrayList.clear(); // {}
        System.out.println("ArrayList size after clear: " + arrayList.size());  // 0
		
	}
}

ArrayList是Java中非常常用的集合类,适用于需要动态数组的场景。它提供了丰富的方法来添加、删除、获取和修改元素,并且由于底层是数组实现,支持快速的随机访问。然而,由于不是线程安全的,在多线程环境中使用时需要注意同步问题。

第五节 迭代器Iterator

前面例子中多次在遍历一组数据元素时使用到了迭代器,那么什么是迭代器呢?

迭代器(Iterator)是Java集合框架中用来遍历集合元素的重要工具。通过迭代器,可以在不暴露集合内部结构的情况下,按顺序访问集合中的每个元素。

迭代器的基本概念

  • Iterator接口Iterator是一个接口,定义了用于遍历集合的标准方法。
  • 泛型支持Iterator接口是泛型的,可以与不同类型的集合一起使用。
  • 集合框架支持:所有实现了Collection接口的集合类都提供了返回Iteratoriterator()方法。

Iterator接口的常用方法

  • boolean hasNext():如果仍有元素可以迭代,则返回true
  • E next():返回迭代的下一个元素。
  • void remove():从集合中移除上一次next()方法返回的元素(可选操作)。

Iterator的使用示例

在Java中,Iterator是一个接口。接口本身不能直接使用,但我们可以通过集合类提供的iterator()方法获取Iterator接口的实现对象,然后使用该对象来调用Iterator接口的方法。

以下是一个使用Iterator遍历ArrayList的示例:

import java.util.ArrayList;
import java.util.Iterator;

public class IteratorExample {
    public static void main(String[] args) {
        // 创建一个ArrayList并添加元素
        ArrayList<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 获取ArrayList的迭代器
        Iterator<String> iterator = list.iterator();  // 集合类提供的`iterator()`方法获取`Iterator`接口的实现对象

        // 使用迭代器遍历集合
        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);
        }
    }
}

使用remove()方法

remove()方法可以在迭代过程中删除当前元素,但需要注意调用顺序:

import java.util.ArrayList;
import java.util.Iterator;

public class IteratorRemoveExample {
    public static void main(String[] args) {
        // 创建一个ArrayList并添加元素
        ArrayList<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 获取ArrayList的迭代器
        Iterator<String> iterator = list.iterator();

        // 遍历集合并删除元素
        while (iterator.hasNext()) {
            String element = iterator.next();
            if (element.equals("Banana")) {
                iterator.remove();
            }
        }

        // 输出修改后的集合
        System.out.println(list);  // 输出: [Apple, Cherry]
    }
}

ListIterator接口

ListIteratorIterator的子接口,专门用于列表的双向遍历。它增加了一些新的方法:

  • boolean hasPrevious():如果有前一个元素,则返回true
  • E previous():返回前一个元素。
  • int nextIndex():返回下一个元素的索引。
  • int previousIndex():返回前一个元素的索引。
  • void set(E e):替换最近返回的元素。
  • void add(E e):在当前位置插入元素。
import java.util.ArrayList;
import java.util.ListIterator;

public class ListIteratorExample {
    public static void main(String[] args) {
        // 创建一个ArrayList并添加元素
        ArrayList<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 获取ArrayList的ListIterator
        ListIterator<String> listIterator = list.listIterator();

        // 使用ListIterator双向遍历集合
        while (listIterator.hasNext()) {
            System.out.println("Next: " + listIterator.next());
        }

        while (listIterator.hasPrevious()) {
            System.out.println("Previous: " + listIterator.previous());
        }
    }
}

for-each循环与迭代器

在大多数情况下,可以使用增强型for循环(for-each)来遍历集合,它实际上是基于迭代器实现的:

import java.util.ArrayList;

public class ForEachExample {
    public static void main(String[] args) {
        // 创建一个ArrayList并添加元素
        ArrayList<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 使用增强型for循环遍历集合
        for (String element : list) {
            System.out.println(element);
        }
    }
}

注意事项

  • 并发修改:在遍历集合时,如果集合被修改,Iterator会抛出ConcurrentModificationException。通过Iteratorremove()方法删除元素不会抛出该异常。
  • 线程安全Iterator不是线程安全的,如果在多线程环境中使用,需手动进行同步。

迭代器是Java集合框架中用来遍历集合的强大工具。通过Iterator接口和其子接口ListIterator,可以方便地遍历和操作集合元素。

那么,问题又出现了,前文多次提到Java集合框架,那么什么是java集合框架?框架中包括了哪些工具?他们如何使用又有是什么不同呢?

第六节 Java的集合框架

Java的集合框架

早在 Java 2 中之前,Java 就提供了特设类。比如:Dictionary, Vector, Stack, 和 Properties 这些类用来存储和操作对象组。

虽然这些类都非常有用,但是它们缺少一个核心的,统一的主题。由于这个原因,使用 Vector 类的方式和使用 Properties 类的方式有着很大不同。

集合框架被设计成要满足以下几个目标。

  • 该框架必须是高性能的。基本集合(动态数组,链表,树,哈希表)的实现也必须是高效的。
  • 该框架允许不同类型的集合,以类似的方式工作,具有高度的互操作性。
  • 对一个集合的扩展和适应必须是简单的。

为此,整个集合框架就围绕一组标准接口而设计。你可以直接使用这些接口的标准实现,诸如: LinkedList, HashSet, 和 TreeSet 等,除此之外你也可以通过这些接口实现自己的集合。

Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:

  • **接口:**是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象
  • **实现(类):**是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
  • **算法:**是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序,这些算法实现了多态,那是因为相同的方法可以在相似的接口上有着不同的实现。

除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。

【添油加醋的Java基础】第五章 数组与字符串_数组_02

Java 集合框架提供了一套性能优良,使用方便的接口和类,java集合框架位于java.util包中, 所以当使用集合框架的时候需要进行导包。

数组、集合、列表与映射

在 Java 中,数组(Array)、集合(Collection)、列表(List)和映射(Map)是几种常用的数据结构,每种数据结构都有不同的特性和使用场景。

数组 (Array)

特点

  • 固定大小,一旦创建不能改变长度。
  • 可以存储基本类型或对象。
  • 访问速度快,通过索引访问元素。
  • 适合需要固定大小、频繁读写的场景。

示例

public class ArrayExample {
    public static void main(String[] args) {
        // 创建一个整数数组
        int[] intArray = new int[5];
        // 为数组赋值
        intArray[0] = 10;
        intArray[1] = 20;
        intArray[2] = 30;
        intArray[3] = 40;
        intArray[4] = 50;

        // 访问数组元素
        for (int i = 0; i < intArray.length; i++) {
            System.out.println("Element at index " + i + ": " + intArray[i]);
        }
    }
}
集合 (Collection)

特点

  • Collection是一个接口,不能直接实例化。
  • Collection接口的常用子接口包括ListSetQueue
  • Collection接口提供了常用的方法,如addremovesize等。

示例

import java.util.ArrayList;
import java.util.Collection;

public class CollectionExample {
    public static void main(String[] args) {
        Collection<String> collection = new ArrayList<>();  // ArrayList是具体的实现类
        collection.add("Apple");
        collection.add("Banana");
        collection.add("Cherry");

        for (String item : collection) {
            System.out.println(item);
        }
    }
}
列表 (List)

特点

  • List接口继承自Collection接口。
  • 允许重复的元素。
  • 保持元素插入的顺序。
  • 通过索引访问元素。

常见实现类

  • ArrayList:ArrayList 是一个数组队列

【添油加醋的Java基础】第五章 数组与字符串_数组_03

  • LinkedList

链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。

链表可分为单向链表和双向链表。

一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。

【添油加醋的Java基础】第五章 数组与字符串_数组_04

一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。

【添油加醋的Java基础】第五章 数组与字符串_夏明亮_05

Java LinkedList(链表) 类似于 ArrayList,是一种常用的数据容器。

与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。

  • Vector

前文有专门的章节说明。

示例

import java.util.ArrayList;
import java.util.List;

public class ListExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 访问列表元素
        for (int i = 0; i < list.size(); i++) {
            System.out.println("Element at index " + i + ": " + list.get(i));
        }
    }
}
映射 (Map)

特点

  • Map接口不属于Collection接口的子接口。
  • 存储键值对(key-value pairs)。
  • 键唯一,值可以重复。
  • 常用方法包括putgetremove等。

常见实现类

  • HashMap

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。

HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。

HashMap 是无序的,即不会记录插入的顺序。

HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。

【添油加醋的Java基础】第五章 数组与字符串_夏明亮_06

  • TreeMap
  • LinkedHashMap

示例

import java.util.HashMap;
import java.util.Map;

public class MapExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("Apple", 1);
        map.put("Banana", 2);
        map.put("Cherry", 3);

        // 访问映射中的元素
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
        }
    }
}
区别和联系

区别

  • 数组:固定大小,元素类型固定,通过索引访问,效率高,适用于大小固定、元素类型已知的场景。
  • 集合:包含多个子接口,适用于不同的数据结构和需求,灵活性高。
  • 列表:有序,允许重复元素,通过索引访问元素,适用于需要顺序访问和修改的场景。
  • 映射:存储键值对,键唯一,适用于需要快速查找和更新的场景。

联系

  • 集合、列表List接口继承自Collection接口,具有集合的所有特性,同时提供了按索引访问的功能。
  • 数组与集合、列表:可以通过Arrays.asList方法将数组转换为列表,但转换后的列表是固定大小的。
  • 映射:不属于Collection接口,但与集合和列表一样,都是Java中常用的数据结构,用于不同的场景。

实际使用建议

  • 数组:适用于固定大小的集合,频繁读写操作。
  • 列表:适用于动态大小的有序集合,频繁插入、删除、修改操作。
  • 集合:适用于需要去重的集合。
  • 映射:适用于键值对数据结构,快速查找和更新。

第七节 泛型Genericity

在Java中,类模板(或称为泛型类)允许我们定义类、接口或方法时使用类型参数,从而使代码更加通用和类型安全。泛型可以帮助我们在编写代码时避免类型转换,并在编译时检测类型错误。

泛型类

定义和使用

我们可以使用尖括号 <> 来定义泛型类,其中类型参数通常用大写字母表示,如 TEKV 等。

package com.xml.practise;

public class Test01 {
	public static void main(String[] args) {
		// 创建一个保存整数的 Box
        Box<Integer> integerBox = new Box<>();   // T自动用Integer替代
        integerBox.set(10);
        System.out.println("Integer Value: " + integerBox.get());

        // 创建一个保存字符串的 Box
        Box<String> stringBox = new Box<>();   // T自动用String替代
        stringBox.set("Hello World");
        System.out.println("String Value: " + stringBox.get());
	}

}


// 定义一个泛型类
class Box<X>{   // 习惯上大家约定俗成使用T而不是X;但并不是强制必须使用T
	private X value;
	
	// 设置值
    public void set(X value) {
        this.value = value;
    }

    // 获取值
    public X get() {
        return this.value;
    }
}

我们定义了一个泛型类 Box,它可以存储任意类型的值。在主方法中,我们分别创建了 Box<Integer>Box<String> 来存储整数和字符串。

这样我们并没有分别为IntegerString类型的变量处理创建各自的类;而是使用泛型的通用类实现了对两种甚至更多类型数据的兼容性处理。

泛型方法

除了泛型类,我们还可以定义泛型方法。泛型方法的类型参数放在返回类型之前。

package com.xml.practise;

public class Test02 {
	public static void main(String[] args) {
		Test a = new Test();
		
		// 申明并初始化一些数组
		Integer[] x = {1,2,3,4,5,6};
		String[] y = {"hello", "world", "i", "love", "you", "!"};
		int[] z = {5,4,3,2,1};  // 基本类型的数组
		
		// 使用泛型方法
		a.printArray(x);
		a.printArray(y);
		// a.printArray(z);  // 错误用法,泛型不支持基本类型;The method printArray(X[]) in the type Test is not applicable for the arguments (int[])
	}
}

class Test{
	<X> void printArray(X[] arr) { // 泛型方法
		for(X i : arr) {
			System.out.println("element: " + i);
		}
	}
}

在这个示例中,我们定义了一个泛型接口 Pair,并通过 OrderedPair 类实现了该接口。通过泛型接口,我们可以定义可以处理任意类型键值对的类。

泛型的优势

  • 类型安全:在编译时检查类型错误,避免运行时出现 ClassCastException
  • 代码重用:编写通用的代码,提高代码的复用性。
  • 可读性:代码更简洁,易读,易维护。

通过泛型,Java 代码变得更加灵活和类型安全,提高了代码的可读性和复用性。无论是类、方法还是接口,泛型都能很好地帮助我们解决类型不确定的问题。

本章小结

习题