• Java
  • 编程

TIP

记录Java学习过程中的困惑之处 📖

# 概述

# 安装 Java

# 1)下载安装包

进入 ORACLE 官网:https://www.oracle.com/ (opens new window) ,点击顶部导航栏的 Products ,找到 Java (位于左下角),就到了 Java 介绍界面。 接着右上角的 Download Java,选择合适的版本(企业开发一般用 JDK 8,学习可安装 JDK 17)下载即可。

图1.1 直达网址:https://www.oracle.com/java/technologies/downloads/#jdk17-windows

本人安装的是 JDK 17 ,并且放在 java 文件夹下。

# 2)安装

这一步按照指示操作即可,有一点需要注意,在选择安装路径的时候,路径必须不能是中文或者出现空格,否则后续可能触发一系列莫名其妙的 bug。

# 3)验证是否安装成功

打开终端/cmd,输入 java 和 javac 命令,若出现以下内容,便是安装成功。

图1.2 验证 java

图1.3 验证 javac

图1.4 查看安装的版本

图1.5 查看 java 的来源:位于 java 下的 bin 目录

# 前置知识

# java 和 javac

了解JDK中的 Java、Javac 的基本作用:**javac.exe**是编译工具,**java.exe**** 是执行工具**。

说明:将来我们写好的 Java 程序都是高级语言,计算机底层是硬件不能识别这些语言,必须先通过 Javac 编译工具进行翻译,然后再通过 Java 执行工具执行才可以驱动机器干活。

# java 程序开发三部曲

图1.6 java 开发三部曲示意图

Notes:

  • 建议代码文件名全英文,首字母大写,满足驼峰模式,源代码文件的后缀必须是 .java
  • 文件名称必须与代码中的类名称一致,比如文件名为 Hello.java,那么文件里的类就必须是 public class Hello { ... }
  • 编译:javac Hello.java,执行:java Hello。(特殊地,JDK 11 开始支持 java 命令直接执行,如 java Hello.java
  • BUG —— 创始人:格蕾丝.赫伯

# JDK 组成

图1.7 JDK 组成成分

图中,javac 命令负责将程序员编写的 java 文件编译成字节码文件 .class,接着 JVM 负责运行 .class,而运行过程中用到了核心类库提供的便捷接口。

Java 程序的内存分配和回收都是由 JRE 在后台自动进行的,JRE 会负责回收那些不再使用的内容,这种机制被称为 垃圾回收(Garbage Collection, GC),垃圾回收是一种动态存储管理技术。

# 环境变量

Path 环境变量:用于记住程序路径,方便在 终端/cmd 的任意目录启动程序。

JAVA_HOME 环境变量:告诉操作系统JDK安装在了哪个位置(将来其他技术要通过这个环境变量找JDK)

以前的老版本的JDK在安装的是没有自动配置Path环境变量的,此时必需要自己配置Path环境变量。

较新版本的JDK只是自动配置了Path,没有自动配置JAVA_HOME。

# 安装 Java 集成开发环境 — IDEA

# 下载和安装

  1. IDE, Integrated Development Environment

把代码编写,编译,执行等多种功能综合到一起的开发工具,可以进行代码智能提示,错误提醒,项目管理等等。常见的 Java IDE 工具有:Eclipse、MyEclipse、IntelliJ IDEA、Jbuilder、NetBeans等。

  1. 下载 & 安装 IDEA

下载链接:https://www.jetbrains.com/idea/ (opens new window)

安装方式:基本上是傻瓜式安装,建议修改安装路径(不要安装在有空格和中文的路径下)。

图1.8 IDEA 快捷键

# 管理 Java 程序的结构

为了便于管理项目代码,将 Java 程序划分以下四类:

  • project 项目/工程
  • module 模块
  • package
  • class

示例:

image.png

# Java 各数据类型的存储原理

程序都是在计算机中的内存中执行的,Java 编译后会生成 .class文件,然后把这个文件提取到正在运行的 JVM 中执行。

Java 内存分配:方法区 + 栈 + 堆

图1.9 Java 内存的三个分区

# 数组的存储和执行

图1.10 基本类型数据的值直接存储在栈内存;数组的元素值存储在堆内存连续的空间中,把该连续空间的首地址存储在栈内存中

# 数据类型和运算符

# 基本数据类型

我们常把 Java 里的基本数据类型分为 4 类:1)整数类型;2)字符类型;3)浮点类型;4)布尔类型。

八个基本类型及其对应的存储大小(bit):

  • short/16
  • int/32
  • long/64
  • byte/8
  • char/16
  • float/32
  • double/64
  • boolean/1

值得注意的是,字符串不是基本数据类型,字符串是一个类,是一个引用数据类型。

# 缓存池/常量池

包装类 缓存池范围
Byte -128 ~ 127
Short -128 ~ 127
Integer -128 ~ 127 (可以通过 -XX:AutoBoxCacheMax 调整上限)
Long -128 ~ 127
Character '\u0000' ~ '\u007F'
Boolean true 和 false

# 装箱&拆箱

Integer a = 8;  // 装箱  ==> Integer.valueOf(8)
int b = a;      // 拆箱  ==> Integer.intValue()

// 自动装箱利用了缓存池,而 new Integer() 则没有利用缓存。
1
2
3
4
1
2
3
4
提问:为什么有栈内存和堆内存之分?

当一个方法执行时,每个方法都会建立自己的内存栈 ,在这个方法内定义的变量将会逐个放入这块栈内存里, 随着方法的执行结束,这个方法的内存栈也将自然销毁 。因此,所有在方法中定义的局部变量都是放在栈内存中的;

在程序中创建一个对象时,这个对象将被保存在运行的数据区中 ,以便反复利用(因为对象的创建成本通常较大), 这个运行时数据区就是堆内存 。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(在方法的参数传递时很常见),则这个对象依然不会被销毁。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收器才会在合适的时候回收它。

# String Pool

String.intern(): https://www.bilibili.com/video/BV1WK4y1M77t

# 面向对象

Java 是面向对象的程序设计语言,提供了定义类、成员变量、方法等最基本的功能。Java 拥有面向对象的三大特征:封装、继承和多态。

  • 封装 — Java 提供了 privateprotectedpublic 三个访问控制修饰符来实现良好的封装
  • 继承 — Java 提供了 extends 关键字来让子类继承父类,子类继承父类就可以继承到父类的的成员变量和方法,如果访问控制允许,子类实例可以直接调用父类里定义的方法。 继承是实现类复用的重要手段
  • 多态 — 使用继承关系来实现复用时,子类对象可以直接赋给父类变量,这个变量具有多态性,编程更加灵活。

# 类和对象

类是面向对象的重要内容。在前面,我们已经知道类可以分为基本数据类型和引用类型,那么在这里,可以把类当成一种自定义的引用类型。因此可以使用类来定义变量(如 User u = new User(...);),这里的 User 是自定义的类,变量 u 就是引用类型。

定义类的简单语法如下:

[修饰符] class 类名
{
  构造器
  成员变量
  方法
}
1
2
3
4
5
6
1
2
3
4
5
6

修饰符可以是 publicfinalabstract,或者完全省略这三个修饰符。需要指出的是,static 修饰的成员不能访问没有 static 修饰的成员。定义好类之后,在其他地方可以通过 new 关键字来调用构造器,从而返回该类的实例。

# Java 基础类库

Java 提供了 StringStringBufferStringBuilder 来处理字符串,还提供了 DateCalendar 来处理日期、时间,其中 Date 是一个已经过时的 API ,通常推荐使用 Calendar 来处理日期和时间。从 JDK 1.4 以后,Jave 也增加了对正则表达式的支持。

# Java 集合

# Java 集合概述

为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java 提供了 集合类 。集合类主要负责保存、盛装其他数据,因此集合类也称为 容器类 。所有的集合类都位于 java.util 包下,后来为了处理多线程环境下的并发安全问题, Java 5 还在 java.util.concurrent 包下提供了一些多线程支持的集合类。

Java 的集合类主要由两个接口派生而出: CollectionMapCollectionMap 是 Java 集合框架的根接口,这两个接口又包含了一些子接口和实现类。

对于 SetListQueueMap 四种集合,最常用的实现类分别是 HashSetTreeSetArrayListArrayDequeLinkedListHashMapTreeMap 等实现类。

# Collection 和 Iterator 接口

Collection 接口是 ListSetQueue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 ListQueue 集合。 Collection 接口里定义了如下操作集合元素的方法:

方法 解释
boolean add(Object o) 该方法把集合 c 里的所有元素添加到指定集合里。如果集合对象被添加操作改变了,则返回 true
boolean addAll(Collection c) 该方法把集合 c 里的所有元素添加到指定集合里。如果集合对象被添加操作改变了,则返回 true
boolean contains(Object o) 返回集合里是否包含指定元素
Iterator iterator() 返回一个 Iterator 对象,用于遍历集合里的元素
boolean remove(Object o) 删除集合中的指定元素 o ,当集合中包含了一个或多个元素 o 时,该方法只删除第一个符合条件的元素,该方法将返回 true
boolean retainAll(Collection c) 从集合中删除集合 c 里不包含的元素(当相遇把调用该方法的集合变成该集合和集合 c 的交集),如果该操作改变了调用该方法的集合,则该方法返回 true
Object[] toArray() 把集合转换成一个数组,所有的集合元素变成对应的数据元素
int size() 返回集合里元素的个数

# 使用 Lambda 表达式遍历集合

Java 8 为 Iterable 接口新增了一个 forEach(Consumer action) 默认方法,该方法所需参数的类型是一个函数式接口,而 Iterable 接口是 Collection 接口的父接口,因此 Collection 集合也可直接调用该方法。

当程序调用 IterableforEach 遍历集合元素时,程序会一次将集合元素传给 Consumeraccept(T t) 方法。正因为 Consumer 是函数式接口,因此可以使用 Lambda 表达式来遍历集合元素。

public class CollectionEach {
  public static void main(String[] args) {
    // 创建一个集合
    Collection books = new HashSet();
    books.add("a");
    books.add("b");
    books.add("c");
    // 调用 forEach() 方法遍历集合
    // forEach() 会自动将集合元素逐个传给 Lambda 表达式的形参 obj
    books.forEach(obj -> System.out.println(obj));
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12

# 使用 Java 8 增强的 Iterator 遍历集合元素

Iterator 接口也是 Java 集合框架的成员,但它与 Collection 系列、 Map 系列的集合不一样: CollectionMap 系列主要用于盛装其他对象,而 Iterator 则主要用于遍历(即迭代访问) Collection 集合中的元素, Iterator 对象也称为 迭代器

Iterator 接口隐藏了各种 Collection 实现类的底层细节,向应用程序提供了遍历 Collection 集合元素的统一编程接口。 Iterator 接口里定义了如下 4 个方法:

方法 解释
boolean hasNext() 如果被迭代的集合元素还没被遍历完,则返回 true
Object next() 返回集合里的下一个元素
void remove() 删除集合里上一次 next 方法返回的元素
void forEachRemaining(Consumer action) Java 8 为 Iterator 新增的默认方法,可使用 Lambda 表达式来遍历集合元素
public class IteratorTest {
  public static void main(String[] args) {
    Collection books = new HashSet();
    books.add("a");
    books.add("b");
    books.add("c");

    // 获取 books 集合对应的迭代器
    Iterator it = books.iterator();

    // 【1】
    while (it.hasNext())
    {
      // next 方法返回的数据类型是 Object 类型,因此需要强制类型转换
      String book = (String) it.next();
      System.out.println(book);
      if (book.equals("b")) {
        it.remove();
      }
    }

    System.out.println(books);

    // 【2】 使用 Lambda 表达式遍历 Iterator
    it.forEachRemaining(obj -> System.out.println(obj));

    // 【3】 使用 foreach 循环遍历集合元素
    for (Object obj : books) {
      System.out.println(obj);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# Set 集合

Set 集合类似于一个罐子,程序把多个对象“丢进” Set 集合,而 Set 集合不会记住元素的添加顺序。实际上 Set 就是 Collection ,只是行为略有不同( Set 不允许包含重复元素)。基于 Set 有三个实现类: HashSetTreeSetEnumSet

Set 既然称为集合,顾名思义, 其不允许包含相同的元素

# HashSet 类

HashSetSet 接口的典型实现,大多数时候使用 Set 时就是使用这个实现类。 HashSet 按哈希算法来存储集合中的元素,因此具有很好的存取和查找性能。

当向 HashSet 集合中存入一个元素时, HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据该 hashCode 值决定该对象在 HashSet 中的存储位置。如果有两个元素通过 equals() 方法比较返回 true ,但它们的 hashCode() 方法返回值不相等, HashSet 将会把它们存储在不同的位置,依然添加成功。也就是说,HashSet 集合判断两个元素相等的标准是两个对象通过 equals() 方法比较相等,并且两个对象的 hashCode() 方法返回值也相等

注意

所以,这里有一个注意点 :当把一个对象放入 HashSet 中时,如果需要重写该对象对应类的 equals() 方法,则也应该重写其 hashCode() 方法。规则是:如果两个对象通过 equals() 方法比较返回 true ,这两个对象的 hashCode 值也应该相同。

如果两个对象通过 equals() 返回 true ,但这两个对象的 hashCode() 返回不同的 hashCode 值时,这导致 HashSet 把这两个对象保存在哈希表的不同位置,这与 Set 集合的规则冲突了。

如果两个对象的 hashCode() 返回的 hashCode 值相同,但通过 equals() 返回 false 时,则更麻烦。因为这两个对象的 hashCode 值相同, HashSet 试图把它们保存在同一个位置,但又会造成冲突,所以会在这个位置用链式结构来保存多个对象,将会导致性能下降。

提问:hashCode() 方法对于 HashSet 是不是十分重要?

哈希算法的功能是,它能保证快速查找被检索的对象 ,哈希算法的价值在于速度。当需要查询集合中某个元素时,哈希算法可以直接根据该元素的 hashCode 值计算出该元素的存储位置,从而快速定位该元素。

当程序向 HashSet 集合中添加元素时, HashSet 会根据该元素的 hashCode 值来计算它的存储位置,这样也可快速定位该元素。

当从 HashSet 中访问元素时, HashSet 先计算该元素的 hashCode 值(也就是调用该对象的 hashCode() 方法的返回值),然后直接到该 hashCode 值对应的位置去取出该元素——这就是 HashSet 速度很快的原因。

# TreeSet 类

TreeSetSortedSet 接口的实现类,正如 SortedSet 名字所暗示的, TreeSet 可以确保集合元素处于排序状态。

TreeSet 是根据元素实际值的大小来进行排序的。与 HashSet 集合采用哈希算法来决定元素的存储位置不同, TreeSet 采用红黑树的数据结构来存储集合元素TreeSet 支持两种排序方法:自然排序(默认)和定制排序。

  1. 自然排序

TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种方式就是自然排序。

  1. 定制排序

自然排序只能根据集合元素的大小将它们进行升序排序。如果需要降序或者按照自己的规则进行排序,则需要定制排序。

定制排序需要在创建 TreeSet 集合对象时,提供一个 Comparator 对象与该 TreeSet 集合关联,由该 Comparator 对象负责集合元素的排序逻辑。由于 Comparator 是一个函数式接口,因此可使用 Lambda 表达式来代替 Comparator 对象。

class M {
  int age;
  public M(int age) {
    this.age = age;
  }
}

public class TreeSetTest {
  public static void main(String[] args) {
    // 按照年龄降序排序
    // 此处 Lambda 表达式的目标类型是 Comparator
    TreeSet ts = new TreeSet((o1, o2) -> {
      M m1 = (M)o1;
      M m2 = (M)o2;
      // 根据 M 对象的 age 属性来决定大小, age 越大, M 对象反而越小
      return m1.age > m2.age ? -1 : (m1.age < m2.age ? 1 : 0);
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

TreeSet 判断两个集合元素相等的标准是:通过 Comparator 或 Lambda 表达式比较两个元素返回了 0 ,这样 TreeSet 不会把第二个元素添加到集合中。

# 各 Set 实现类的性能分析

DANGER

必须指出的是,Set 的三个实现类都是线程不安全的。如果有多个线程同时访问一个 Set 集合,并且有超过一个线程修改了该 Set 集合,则必须手动保证该 Set 集合的同步性。

通常可以通过 Collections 工具类的 synchronizedSortedSet 方法来“包装”该 Set 集合。此操作最好在创建时进行,以防止对 Set 集合的意外非同步访问。

# 注解(Annotation)

从 JDK 5 开始,Java 增加了对元数据(MeteData)的支持,也就是 Annotation 。注解是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用注解,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。

# 基本注解

Java 提供了 5 个基本注解的用法,使用注解时要在其前面增加 @ 符号,并把该注解当成一个修饰符使用,用于修饰它支持的程序元素。

  • @Override
  • @Deprecated
  • @SuppressWarnings
  • @SafeVarags (Java 7 新增)
  • @FunctionalInterface (Java 8 新增)

这 5 个基本注解都定义在 java.lang 包。