JAVA入门学习笔记#02(包装类、引用、数组、集合与泛型)

本文假设读者已有其他面向对象高级语言的基础(如C++基础),在此基础上介绍 Java 的基础知识。第一期介绍了 Java 中的变量、类型、函数、包JAVA入门学习笔记#01(基础概念、基本语法和关键字)

本期主要介绍 Java 中的包装类、引用类型、数组与切片。Java 中循环判断、类与继承、类方法、重写与重载等概念与 C++ 中基本一致,在此不做介绍。

目录

Java 包装类

装箱(Boxing)

拆箱(Unboxing)

说明

Java 基本类型和引用类型

基本数据类型

引用类型

Java 引用

Java 引用和 C++ 指针区别

Java 数组

Java 集合

Collection 接口及其实现类

Map 接口及其实现类

Java 泛型

1. 泛型的定义

2. 泛型的常见语法

3. Collection 的泛型代码示例

4. 泛型等式左边的集合应该是右边的集合或其父类

5. Map 的代码示例

6. 通配符、extends 和 super

7. Java 泛型和 C++ STL


下期讲反射和注解等。

Java 包装类

上篇提到, Java 中的每种基本类型都有对应的包装类,包装类是对象,其成员函数包括很多实用方法,Java 中八种基本类和包装类对应如下:

  1. Integer 对应 int
  2. Double 对应 double
  3. Boolean 对应 boolean
  4. Character 对应 char
  5. Byte 对应 byte
  6. Short 对应 short
  7. Float 对应 float
  8. Long 对应 long

装箱(Boxing)

装箱是指将基本数据类型自动转换为对应的引用数据类型(即包装类)。例如,将一个 int 类型的值转换为 Integer 对象。这是在Java 5 后引入自动装箱之后的一个特性,即 JDK5 之前需要手动创建包装类的实例。

int i = 10;
Integer integer = i; // 自动装箱

拆箱(Unboxing)

拆箱是将包装类的对象转换回基本数据类型。例如,将一个 Integer 对象转换为 int 类型的值。

Integer integer = 20; // 自动装箱
int i = integer; // 自动拆箱

说明

  • 拆装箱使用场景:需将基本数据类型作为参数传递给需要对象的方法,或者存储在集合(如 List 和 Set)中时。
  • 拆装箱涉及对象的创建和销毁,频繁拆装箱会带来过大的性能开销

Java 基本类型和引用类型

在Java中,除了八种基本数据类型之外,所有其他类型都被认为是引用类型。基本数据类型直接存储值,而引用类型存储的是对象的引用,即对象在内存中的地址。

基本数据类型

Java中有八种基本数据类型,分为两种:四种整数类型(byte, short, int, long)、两种浮点类型(float, double)、一个字符类型(char)和一个布尔类型(boolean)。这些类型直接在栈内存中分配空间存储具体的数值,当变量值改变时,实际的值会直接更新。

引用类型

除了基本数据类型,Java中的所有其他类型都属于引用类型,包括但不限于:

  • 类(Class):这是Java中最常见的引用类型,如你自己定义的类、内置的String类等。
  • 数组(Array):数组也是引用类型,即使它们包含基本数据类型,数组变量仍然只是指向数组首地址的引用。
  • 接口(Interface):实现接口的类的实例是引用类型。
  • 枚举(Enum):枚举类型也属于引用类型。
  • 泛型(Generics):使用泛型的类或接口实例化后也是引用类型。
  • 异常(Exception):异常类的实例也是引用类型。
  • 集合框架(Collections):如ListSetMap等集合类的实例。
  • 线程(Thread)Thread类的实例。

引用类型在堆内存中分配空间,而变量(引用)则在栈内存中存储,指向堆中的对象。这意味着,当你创建一个引用类型的变量并初始化它时,你实际上是在栈内存中创建了一个指向堆中对象的引用。当你传递引用类型的变量时,你传递的是这个引用的拷贝,而不是对象本身。例如,考虑下面的代码:

class MyClass {
    int value;
}

public class Main {
    public static void main(String[] args) {
        MyClass obj1 = new MyClass();  // 创建一个可以指向MyClass类型对象的引用
        obj1.value = 5;  // 对引用内容(指针指向的int)赋值
        
        MyClass obj2 = obj1; // 此处传递的是引用
        
        obj2.value = 10;  // 修改了引用内容(指针指向的int)
        
        System.out.println(obj1.value); // 输出: 10
    }
}

在这个例子中,obj1obj2都指向堆中同一个MyClass对象。当我们将obj1赋值给obj2时,实际上是在栈中创建了一个新的引用obj2,但它指向的是obj1所指向的同一个对象。因此,改变obj2.value也会影响到obj1.value的值。

Java 引用

Java 中除了基本类型都是引用类型,而且 Java 中只有值传递(不像引用传递的功能。C++中有引用传递),但是 Java 中的引用类型(依然还是还是值传递,不过传递的是指向对象的地址值)可实现类似 C++ 中引用传递的功能。考虑如下引用类型(一个类):

public class Shuzhi {
    public void test(){
        int shuzhi=1;
    }
}

以上代码完成的操作是:

  1. 创建了该基本数据类型(示例中为int)的引用,引用名为该基本数据类型名(示例中为shuzhi)。
  2. 在栈区创建了该基本数据类型(示例中为int)的变量内容(示例中为1)。
  3. 变量内容在编译器处理下实际为该内容的地址(示例中即1这个代码实际上返回1的地址),将内容的地址赋给引用,完成定义。
  4. 该数值类变量定义后被再次使用时都被会被自动解引用(即根据引用内容自动取内容),因此之后可以继续完成赋值等操作。如 shuzhi=2; 该代码完成的操作为(*shuzhi)=2

byte,char,long,float,double等数据类型引用的创建同int。

在Java中,“引用”和C/C++中的“指针”在概念上有相似之处,因为它们都代表了对对象的间接访问方式。但Java的引用在设计上更加安全,更加面向对象,同时也更加受限,以防止程序员直接控制内存。

Java 引用和 C++ 指针区别

  1. 安全性:Java的引用不允许进行低级的内存操作,例如你不能像C/C++那样直接操纵内存地址,或者做指针运算(如加减操作)。Java的引用只能用来访问对象的方法和字段。

  2. 垃圾回收:Java的引用类型受到垃圾回收器的管理。当一个对象不再被任何引用所指向时,它会被垃圾回收器自动回收,释放其占用的内存。在C/C++中,你需要显式地管理内存的分配和释放。

  3. 类型检查:Java的引用是强类型的,必须与对象的实际类型匹配。这避免了类型不匹配导致的运行时错误。

  4. 空引用:Java中的引用可以是null,表示它们不指向任何对象。这在编程中经常用于初始化阶段或表示对象尚未被创建。

  5. 引用类型:Java有多种引用类型,如强引用、软引用、弱引用和虚引用,它们在垃圾回收过程中的行为各不相同。

Java 数组

数组的声明方式

// 常用数组
int[] arr1;
char[] arr2;
String[] arr3;

// 创建方式1:静态创建
int[] numbers = {1, 2, 3, 4, 5}; // 一维数组
int[][] matrix = new int[3][4]; // 3行4列的二维数组

// 创建方式2:静动态创建
int[] numbers = new int[5];

数组的访问方式同 C++,可直接使用索引访问,如 numbers[0] 可获取第一个元素的值

Arrays 类提供很多内置方法来操作数组,如排序和填充等

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ArraysExamples {

    /**
     * 此示例展示了使用 java.util.Arrays 类的各种方法,
     * 以及如何将数组转换为 List 和使用 Stream API 进行操作。
     */
    public static void main(String[] args) {
        int[] arr = {5, 2, 9, 1};  // 创建一个数组

        Arrays.sort(arr);  // 对数组进行排序

        Arrays.fill(arr, 0);  // 用0值填充整个数组

        int[] copy = Arrays.copyOf(arr, arr.length);  // 复制数组

        copy = Arrays.copyOfRange(arr, 0, 3);  // 复制数组的一部分

        // 比较两个数组是否相等
        boolean isEqual = Arrays.equals(new int[]{7, 7, 7}, copy);

        String str = Arrays.toString(new int[]{1, 2, 3});  // 将数组转换为字符串格式

        int index = Arrays.binarySearch(new int[]{1, 2, 3}, 2);  // 在已排序的数组中搜索一个元素

        // 将数组转换为 List(注意:返回的 List 是数组的视图,而不是数组的实例,)
        List<Integer> list = Arrays.asList(1, 2, 3);

        // 使用 Stream API 来处理数组元素
        long total = Arrays.stream(new int[]{1, 2, 3}).sum();

        // 使用 Stream 过滤数组元素
        List<Integer> filteredList = Arrays.stream(new int[]{1, 2, 3, 4, 5})
                                           .filter(n -> n > 3)
                                           .boxed()
                                           .collect(Collectors.toList());

        // 使用 Lambda 表达式设置数组中所有元素的值
        int[] setAllArray = new int[5];
        Arrays.setAll(setAllArray, i -> i * i);
        // System.out.println("使用 Lambda 设置值后的数组: " + Arrays.toString(setAllArray));
    }
}

Java 集合

Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:ListSetQueue

Java 集合框架如下图所示:

Java集合框架主要包括两大类:CollectionMap。下面将详细介绍每种主要的集合类型,它们对应的数据结构以及适用的场景。

Collection 接口及其实现类

Collection 接口是所有集合类的根接口,它有三个主要的子接口:List, Set 和 Queue。

  1. List

    • ArrayList:基于动态数组实现,适用于需要随机访问元素的场景,提供快速的O(1)时间复杂度的元素访问。
    • LinkedList:基于双向链表实现,适用于频繁插入和删除元素的场景,插入和删除操作的时间复杂度为O(1)。
    • Vector:类似于ArrayList,基于动态数组,但提供线程安全机制,适用于多线程环境。
    • Stack:继承自Vector,实现了一个后进先出(LIFO)的栈,适用于需要栈数据结构的场景,如算法中的递归替代、表达式求值等。
  2. Set

    • HashSet:基于哈希表实现,适用于需要快速查找、插入和删除元素的场景,平均时间复杂度为O(1)。
    • TreeSet:基于红黑树实现,适用于需要自动排序的场景,保证元素按自然顺序或自定义比较器排序,时间复杂度为O(log n)。
    • LinkedHashSet:结合了HashSetLinkedList的特点,保持了元素的插入顺序,适用于需要保留插入顺序的场景。
  3. Queue

    • ArrayDeque:基于数组的循环双端队列,适用于需要两端都可以高效插入和删除元素的场景,如任务调度队列、消息队列等。
  4. Deque(Double Ended Queue)

    • ArrayDequeLinkedList:适用于需要在队列的两端进行插入和删除操作的场景。

Map 接口及其实现类

Map 接口用于存储键值对,其中键是唯一的。

  1. 数据结构Map的主要实现包括HashMap(基于哈希表)、TreeMap(基于红黑树)和LinkedHashMap(基于哈希表+链表)。
  2. 使用场景
    • HashMap:适用于需要快速查找、插入和删除键值对的场景,平均时间复杂度为O(1)。
    • TreeMap:适用于需要自动按键排序的场景,保证键按自然顺序或自定义比较器排序,时间复杂度为O(log n)。
    • LinkedHashMap:保持了键的插入顺序,适用于需要保留插入顺序的场景。
    • Hashtable:基于哈希表,与HashMap类似,但Hashtable是线程同步的,适用于多线程环境,需要线程安全的键值对存储场景。
    • SortedMap(接口):TreeMap是其主要实现,适用于需要保持键值对有序的场景,如排序后的键值对集合。

Java 泛型

Java 泛型是 Java SE 5 引入的一项重要特性,它允许在编译时检查类型安全,并且所有的强制转换都是自动和隐式的,增加了代码的重用性和安全性。泛型使你能够编写类型安全的通用类或方法,而无需在运行时进行类型检查或转换,Java 的泛型有点像 C++ 的STL 和容器。

1. 泛型的定义

泛型本质上是一种参数化类型,也就是说,当我们定义类、接口或方法时,可以将类型作为一个参数。这样做的好处是,你可以编写一个通用的类或方法,它可以针对多种具体类型工作,而无需为每种类型都定义一个单独的类或方法。

2. 泛型的常见语法

泛型的使用通常涉及到类型参数、通配符和类型边界等概念。

  1. 类型参数: 在定义类、接口或方法时,你可以声明一个或多个类型参数。类型参数通常用大写字母表示,如T, E, K, V等。例如:

    public class GenericClass<T> {
        private T value;
        public T getValue() { return value; }
        public void setValue(T value) { this.value = value; }
    }
  2. 通配符: 通配符(?)用于表示未知的类型,它可以接受任何类型的参数的方法,但同时也限制了对参数的操作。如:

    public void printList(List<?> list) {
        for (Object item : list) {
            System.out.println(item);
        }
    }
  3. 类型边界: 类型参数可以有边界,即指定类型参数必须是某一个类的子类或实现某一个接口。例如:

    public class GenericClass<T extends Number> {
        // ...
    }
  4. 类型参数的上限和下限

    • 上限:extends 关键字用于指定类型参数的上限。
    • 下限:super 关键字用于指定类型参数的下限。例如,T super Number 表示 可以是 Number 或其任何超类型。

下面是一个使用泛型的简单示例,展示如何定义一个泛型类和使用它。

// 定义一个泛型类
public class Box<T> {
    private T item;

    public Box(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}

public class Main {
    public static void main(String[] args) {
        // 创建一个可以存储String类型的Box
        // 使用字符串 Hello World 来对 Box 类进行构造
        Box<String> stringBox = new Box<>("Hello, World!");
        System.out.println(stringBox.getItem()); // 输出: Hello, World!

        // 创建一个可以存储Integer类型的Box
        // 使用整数 42 来对 Box 类进行构造
        Box<Integer> intBox = new Box<>(42);
        System.out.println(intBox.getItem()); // 输出: 42
    }
}

在这个示例中,Box类是一个泛型类,它可以用于存储任何类型的对象。在创建Box的实例时,我们指定了具体的类型参数,如StringInteger,这样就可以创建特定类型的Box实例。

注意,在表达式 Box<Integer> intBox = new Box<>(42); 中,<>内的尖括号通常用于指定泛型参数的类型。然而,在Java 7及更高版本中,如果你在声明变量时已经指定了类型参数,那么在实例化集合时可以利用类型推断,此时<>内的类型参数可以省略,这就是所谓的“钻石操作符”。

3. Collection 的泛型代码示例

例如,如果你有以下的代码:

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

在Java 7之前,你需要显式地在ArrayList后面跟上<String>来指定类型参数。但在Java 7之后,由于编译器可以从左边的List<String>推断出你需要的是一个存储String类型的ArrayList,因此你可以省略<String>部分,代码可以简化为:

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

这里的<>(没有指定类型参数)被称为“钻石操作符”,表示编译器应该使用类型推断来确定正确的类型参数。

4. 泛型等式左边的集合应该是右边的集合或其父类

仔细看上述代码,为什么等式左边是“List<String> names = new ArrayList<>();”,为什么不是“ArrayList<String> names = new ArrayList<>();”呢?

在Java中,你可以选择使用接口类型(如List<String>)或者具体的实现类类型(如ArrayList<String>)来声明变量。使用接口类型来声明变量(如List<String> names = new ArrayList<>();)有以下几个好处:

  1. 抽象与实现解耦:当你使用接口类型声明变量时,你将变量的类型与其实现的具体类解耦。这意味着你可以随时改变底层使用的具体实现类(例如,从ArrayList改为LinkedList),而无需修改所有使用该变量的代码。这提高了代码的灵活性和可维护性。

  2. 更好的兼容性和扩展性:由于变量是基于接口声明的,你可以在不影响现有代码的情况下,添加新的实现类。此外,如果你的代码依赖于接口而非具体实现,那么当接口有新的实现时,你的代码可以更容易地利用这些新实现。

  3. 编码风格和最佳实践:许多开发者和团队遵循一种编码规范,即使用接口类型来声明变量,这被认为是一种良好的编程习惯。它强调了代码应该依赖于抽象,而不是具体实现。

然而,使用具体实现类(如ArrayList<String> names = new ArrayList<>();)来声明变量也是可行的,并且在某些情况下可能更直观,尤其是当具体实现的特性对代码逻辑至关重要时。

那如果我直接使用根父类来声明呢?

使用 Collection<String> names = new ArrayList<>();这样的声明方式是完全有效的,而且确实可以进一步提高代码的灵活性,因为 Collection 是所有集合类的顶级接口。但是,这样做也有一些潜在的缺点:

  1. 方法可用性限制Collection 接口提供了集合的基本操作,如 add, remove, contains, isEmpty, size 等,但它并不包含 List 接口中特有的方法,如 get(int index), set(int index, E element), 或者 add(int index, E element)。如果你使用Collection 作为类型,你将无法访问这些 List 特有的方法,这可能会限制你对集合的操作能力。

  2. 类型信息丢失:虽然使用 Collection 可以让你在需要时替换为任何 Collection 的实现,但这也意味着你放弃了 List 或 Set 等特定集合类型的信息。这可能使得代码阅读者需要额外的工作来理解集合的预期行为。

  3. 编译器警告:在某些情况下,如果你使用 Collection 声明的变量试图调用 List 特有的方法,编译器会发出警告,提示你可能的类型不匹配。

在实践中,通常建议使用最具体的接口类型来声明集合变量,比如 List<String> Set<String>,因为这提供了更多类型安全性和方法可用性。然而,如果你确实需要一个高度灵活的容器,或者你不确定未来是否会改变集合的类型,使用Collection也是可以的。但是,你必须确保你的代码仅依赖于Collection接口提供的方法,否则你将需要进行强制类型转换,这可能会带来类型安全风险。

5. Map 的代码示例

Map<String, Object> data = new HashMap<>();

这行代码定义了一个名为data的变量,它是一个Map类型的实例,其中键(key)的类型是String,值(value)的类型是Object。具体解析如下:

  1. Map<String, Object>:这是变量data的类型声明,说明data是一个映射(map),它将String类型的键映射到Object类型的值。在Java中,Map是一个接口,它定义了键值对集合的行为。

  2. new HashMap<>():这是创建一个新的HashMap实例。HashMapMap接口的一个实现类,它提供了基于哈希表的映射,提供了较快的存取速度。<>内部的String, Object是类型参数,指定了HashMap中键和值的类型。

  3. 泛型与类型推断:使用<>内的类型参数,我们可以明确指定HashMap中键和值的类型。在Java 7及以上版本中,由于类型推断的存在,new HashMap<>()可以省略类型参数(即String, Object),编译器会从左边的类型声明Map<String, Object>中推断出类型参数。

通过这行代码,你可以创建一个HashMap实例,用于存储和管理键为String,值为Object的键值对。例如,你可以像下面这样使用data

data.put("name", "John Doe");
data.put("age", 30);
data.put("isStudent", false);

String name = (String) data.get("name");
Integer age = (Integer) data.get("age");
Boolean isStudent = (Boolean) data.get("isStudent");

需要注意的是,因为值的类型是Object,所以在获取值时可能需要进行类型转换,如上述代码中的(String), (Integer), 和 (Boolean)

6. 通配符、extends 和 super

  • extends: 当你使用 extends 关键字时,你是在说这个类型通配符可以匹配指定类的任何子类(包括指定的类本身)。这通常用于生产者场景,即一个方法可能返回一个不确定的子类型。

  • super: 相反,super 关键字用于消费者场景,它指定了类型通配符可以匹配指定类的任何超类(包括指定的类本身)。这意呸着你可以将任何类型的对象放入一个被声明为超类的对象中。

public class GenericExample {

    // 使用 extends 限定类型通配符,这个泛型的类型只能是 Number 或 Number 的子类
    public static void printNumbers(List<? extends Number> numbers) {
        for (Number number : numbers) {
            System.out.println(number);
        }
    }

    // 使用 super 限定类型通配符,确保它是 Integer 或 CInteger 的父类
    public static void addAll(List<? super Integer> list, List<Integer> items) {
        for (Integer item : items) {
            list.add(item);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        List<Double> doubleList = new ArrayList<>();
        List<Comparable> compList = new ArrayList<>();

        // 可以将 intList 或 doubleList 传递给 printNumbers 方法
        printNumbers(intList); // OK
        printNumbers(doubleList); // OK

        // 不能将 compList 传递给 printNumbers,因为 Comparable 不是 Number 或其子类
        // printNumbers(compList); // Error

        // 可以将 compList 传递给 addAll 方法,因为 Integer 是 Comparable
        addAll(compList, intList); // OK

        // 不能将 intList 或 doubleList 传递给 addAll,因为它们不是 List<Comparable>
        // addAll(intList, intList); // Error
        // addAll(doubleList, intList); // Error
    }
}

7. Java 泛型和 C++ STL

  • C++ STL (Standard Template Library):

    • C++中的模板允许你编写能应用于多种数据类型的代码。
    • 在编译时,模板会被实例化为特定类型的代码,这意味着对于每种数据类型,编译器都会生成对应的代码版本。
    • 这导致了类型安全和潜在的运行时性能优势,因为编译器可以根据具体类型优化代码。
    • 编译后的代码可能更大,编译时间也可能更长,因为需要为每个模板实例生成代码。
  • Java 泛型:

    • Java泛型允许你编写类型安全的代码,该代码在编译时检查类型,但不生成特定类型的多个实例。
    • 类型擦除意味着编译后的字节码不包含类型参数信息,所有泛型类型在运行时被视为其原始类型。
    • 这减少了代码的膨胀和编译时间,但可能会限制某些运行时优化,因为类型信息在运行时不可用。
    • Java泛型还包括类型通配符和类型边界等概念,以增加代码的灵活性。

参考链接

  1. Java数组(这一篇就够了)(超详细)
  2. 深入理解Java中的指针——引用
  3.  Java 集合:List, Set, Queue 和 Map
  • 14
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值