一、Java 基础概述

Java 是一种广泛应用的编程语言,其基础内容包括开发入门、阵列、面向对象、常见基本类、集合、IO 流、多线程、异常、反射等。了解这些基础内容对于掌握 Java 编程至关重要。
Java 的基础内容丰富多样,为开发者提供了强大的编程工具和灵活的编程方式。
开发入门
开发入门阶段主要涉及到 Java 开发环境的搭建,包括安装 JDK(Java Development Kit)、配置环境变量等。JDK 是 Java 开发的核心工具包,它包含了 JRE(Java Runtime Environment)和一些开发工具。JRE 又由 JVM(Java Virtual Machine)和 Java 的基本类库组成。JVM 是 Java 能够跨平台的核心,它只认识 .class 文件,能够将字节码指令进行识别并调用操作系统的 API 完成动作。
阵列
在 Java 中,阵列(数组)是一种存储相同数据类型元素的集合。数组可以通过指定元素类型和长度来创建。Java 中的数组具有固定的长度,一旦创建后不能改变大小。常用的数组操作包括遍历数组、获取数组长度、访问特定位置的元素等。例如,可以使用 for 循环遍历数组,通过索引访问数组中的元素。数组在存储和处理一组相关数据时非常有用,但在需要动态改变大小或存储不同类型数据时,可能需要使用集合类。
面向对象
面向对象编程是 Java 的核心编程思想。面向对象编程将现实世界中的事物抽象为程序中的对象,通过封装、继承和多态三大特性来实现代码的可维护性、可扩展性和可重用性。
封装
封装是将数据和操作数据的方法封装在一个类中,通过访问修饰符来控制对类成员的访问。这样可以减少耦合,提高代码的安全性和可维护性。例如,可以将学生的属性(如年龄、姓名、学号等)和行为(如学习)封装在一个学生类中,外部只允许通过公开的方法来访问学生的属性和进行操作。
继承
继承是子类继承父类的特征和行为,使得子类对象具有父类的属性和方法。继承实现了 "is-a" 关系,例如猫是动物的一种,猫类可以继承自动物类。父类引用可以指向子类对象,称为向上转型。继承可以提高代码的重用性,但要遵循里氏替换原则,即子类对象必须能够替换掉所有父类对象。
多态
多态是允许不同子类型的对象对同一消息作出不同的响应。多态分为编译时多态和运行时多态。编译时多态主要指方法的重载,即同一个方法名可以有不同的参数列表。运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。运行时多态有三个条件:继承、覆盖(重写)和向上转型。例如,小猫和小狗都是动物的子类,但是小猫会 "喵喵喵",小狗会 "汪汪汪"。
常见基本类
Java 中有许多常见的基本类可以直接使用,为编程提供了丰富的功能。
Object 类
Object 类是所有类的父类,提供了一些通用的方法,如 equals()、hashCode()、toString() 等。这些方法在比较对象、获取对象哈希值和将对象转换为字符串时非常有用。
包装类
Java 为基本数据类型提供了包装类,如 Integer、Long、Float、Double、Byte、Short、Character、Boolean。这些包装类用于在对象中封装对应的基本数据类型值,提供了一些常用的方法。例如,可以使用 Integer.parseInt() 方法将字符串转换为整数。
Math 类
Math 类提供了各种数学运算和函数,如求绝对值、求平方根、求最大值、最小值等。在进行数学计算时,可以方便地使用 Math 类的方法。
Arrays 类
Arrays 类提供了操作数组的方法,如排序、搜索、拷贝等。例如,可以使用 Arrays.sort() 方法对数组进行升序排列。
Date、Calendar 类
Date 和 Calendar 类用于表示和操作日期和时间数据。可以使用这些类来获取当前时间、进行日期和时间的计算等。
System 类
System 类提供了与系统相关的属性和方法,如获取当前时间、获取系统属性、输入输出等。例如,可以使用 System.out.println() 方法输出信息到控制台。
File 类
File 类用于操作文件和目录。可以使用 File 类来创建、删除、重命名文件和目录,以及检查文件和目录的属性。
Scanner 类
Scanner 类用于读取用户输入。可以使用 Scanner 类从控制台读取用户输入的字符串、整数、浮点数等数据。
Random 类
Random 类用于生成随机数。可以使用 Random 类生成随机整数、随机浮点数等。
集合
Java 集合是用于存储和操作一组对象的数据结构。集合框架提供了一组接口和类,用于处理不同类型的集合数据,如列表、集、映射等。
集合框架的基本概念
Java 集合框架是 Java 编程语言提供的一组类和接口,用于存储、操作和处理数据集合。它提供了各种类型的数据结构,如列表、集合、映射等,以及用于操作和处理这些数据结构的算法和工具。
集合框架的接口包括 Collection、List、Set、Map、Queue 等。这些接口定义了对集合元素的操作方法,如添加、删除、遍历等。集合框架的实现类包括 ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap 等。这些实现类实现了相应的接口,提供了具体的数据结构和算法。
集合框架的接口和实现类
- Collection 接口:是所有集合的基本接口,定义了对集合元素的基本操作。它继承自 Iterable 接口,表示支持迭代访问。
- List 接口:有序、可重复的集合,可以通过索引访问元素。常见的实现类有 ArrayList、LinkedList、Vector。
- Set 接口:无序、不可重复的集合,不允许存储相同元素。常见的实现类有 HashSet、TreeSet、LinkedHashSet。
- Map 接口:表示键值对的集合,每个键都是唯一的。常见的实现类有 HashMap、TreeMap、LinkedHashMap。
- Queue 接口:表示一种特殊的集合,用于实现队列(先进先出)的数据结构。常见的实现类有 LinkedList、PriorityQueue。
集合框架的继承结构
集合框架的继承结构如下:
- Collection 接口是所有集合的基本接口,继承自 Iterable 接口。
- List、Set、Queue 接口继承自 Collection 接口。
- Map 接口与 Collection 接口平行,不继承自 Collection 接口。
各个集合类有不同的特点和适用场景。例如,ArrayList 和 LinkedList 都是实现了 List 接口的集合类,但 ArrayList 基于数组实现,随机访问速度快,插入和删除操作速度较慢;而 LinkedList 基于链表实现,插入和删除操作速度快,随机访问速度较慢。HashSet、TreeSet 和 LinkedHashSet 都是实现了 Set 接口的集合类,但 HashSet 不保证元素的顺序,TreeSet 按照元素的自然顺序或特定比较器定义的顺序来存储元素,LinkedHashSet 按照插入顺序来存储元素。HashMap、TreeMap 和 LinkedHashMap 都是实现了 Map 接口的集合类,但 HashMap 不保证元素的顺序,TreeMap 按照键的自然顺序或特定比较器定义的顺序来存储元素,LinkedHashMap 按照插入顺序或访问顺序来存储元素。
IO 流
Java 的 IO(输入输出)流用于读取和写入数据。IO 流可以分为字节流和字符流。字节流用于处理二进制数据,字符流用于处理文本数据。
Java 提供了丰富的 IO 流类,如 FileInputStream、FileOutputStream、BufferedInputStream、BufferedOutputStream、InputStreamReader、OutputStreamWriter 等。可以使用这些类来读取和写入文件、网络连接等。
例如,可以使用 FileInputStream 和 FileOutputStream 来读取和写入文件。使用 BufferedInputStream 和 BufferedOutputStream 可以提高读写效率。使用 InputStreamReader 和 OutputStreamWriter 可以在字节流和字符流之间进行转换。
多线程
多线程是一种程序执行方式,允许多个线程在同一个进程中并发执行。每个线程是一个独立的执行路径,可以同时执行多个任务。多线程可以显著提高程序的运行效率,特别是在处理 I/O 密集型和计算密集型任务时。
创建线程的两种方式
- 继承 Thread 类:需要继承 Thread 类,并重写其 run 方法。然后创建线程对象并调用 start 方法启动线程。
- 实现 Runnable 接口:定义一个类实现 Runnable 接口,并重写 run 方法。然后创建线程对象并将实现了 Runnable 接口的类的对象作为参数传递给 Thread 类的构造函数,最后调用 start 方法启动线程。
实现 Runnable 接口比继承 Thread 类更为灵活,因为 Java 不支持多继承,而实现 Runnable 接口可以避免这个限制。
线程的生命周期
线程在 Java 中的生命周期包括以下几个状态:
- 新建状态(New):线程对象被创建但未启动。
- 就绪状态(Runnable):线程已经准备好运行,等待 CPU 调度。
- 运行状态(Running):线程正在执行 run 方法的代码。
- 阻塞状态(Blocked):线程因为某种原因被挂起,等待某个条件满足后重新进入就绪状态。
- 死亡状态(Dead):线程执行完毕或被终止。
线程的同步
在多线程环境下,多个线程可能会同时访问共享资源,导致数据不一致。为了避免这种情况,需要对共享资源进行同步。
- 同步方法:使用 synchronized 关键字可以将方法声明为同步方法。只有一个线程可以在同一时间执行同步方法。
- 同步代码块:如果只需要对部分代码进行同步,可以使用同步代码块。使用 synchronized 关键字和一个对象作为锁来实现同步代码块。
线程间通信
在多线程编程中,线程间的通信是非常重要的。Java 提供了多种方式来实现线程间通信,常用的方法有 wait、notify 和 notifyAll。
例如,可以使用 wait 和 notify 方法来实现生产者 - 消费者模式。生产者线程在生产数据后调用 notify 方法通知消费者线程,消费者线程在等待数据时调用 wait 方法等待生产者线程的通知。
高级线程工具
- 线程池:Java 中的 Executor 框架提供了一个强大的线程池管理机制,能够有效地控制并发线程的数量,提高系统性能。可以使用 Executors 类创建不同类型的线程池,如固定大小的线程池、可缓存的线程池等。
- Callable 和 Future:Callable 接口类似于 Runnable,但它可以返回结果,并且可以抛出异常。与 Callable 配合使用的 Future 接口用于获取异步执行的结果。可以使用 ExecutorService 的 submit 方法提交 Callable 任务,并使用 Future 的 get 方法获取任务的结果。
异常
在 Java 中,异常是程序在运行过程中发生的错误或异常情况。Java 提供了异常处理机制来处理这些异常,以提高程序的健壮性。
Java 中的异常分为受检异常(Checked Exception)和非受检异常(Unchecked Exception)。受检异常需要在代码中显式地进行处理,否则编译器会报错。非受检异常不需要在代码中显式地进行处理,但也可以进行处理以提高程序的健壮性。
Java 中的异常处理使用 try-catch 语句块来捕获和处理异常。可以在 try 块中放置可能会抛出异常的代码,在 catch 块中处理异常。还可以使用 finally 块来执行无论是否发生异常都要执行的代码。
例如,可以使用以下方式处理异常:
|-----------------------------------------------------------------------------------------|
| try { // 可能会抛出异常的代码 } catch (Exception e) { // 处理异常的代码 } finally { // 无论是否发生异常都要执行的代码 } |
反射
反射是 Java 中的一种机制,它允许程序在运行时动态地获取类的信息、创建对象、调用方法等。反射可以提高程序的灵活性和可扩展性。
通过反射,可以获取类的名称、属性、方法、构造函数等信息。可以使用反射来创建对象、调用方法、设置属性值等。
例如,可以使用以下方式获取类的信息并创建对象:
|--------------------------------------------------------------------------------------------|
| Class<?> clazz = Class.forName("com.example.MyClass"); Object obj = clazz.newInstance(); |
以上是 Java 基础内容的概述,掌握这些基础内容对于深入学习 Java 编程至关重要。
二、Java 对象是什么
(一)对象与类的概念
- 类是一个模板或蓝图,用于定义对象的结构和行为,包含数据成员和函数成员。
- 类是对一类事物的抽象描述,它定义了这类事物的共同属性和行为。比如,一张汽车设计图纸可以看作是一个类的描述,它规定了汽车应该具有的结构和功能。在 Java 中,类是通过关键字class来定义的,它包含了成员变量(数据成员)和成员方法(函数成员)。成员变量用于描述对象的状态,而成员方法用于定义对象的行为。
- 对象是类的实例,具有独特的属性和行为,占用内存空间,代表真实世界中的某个实体。
- 对象是根据类的定义创建出来的具体实例。每个对象都有自己独立的内存空间,并且具有类中定义的属性和行为。例如,根据汽车设计图纸制造出来的一辆具体的汽车就是一个对象。对象可以通过属性来存储数据,并通过方法来执行特定的操作。
(二)代码举例说明
- 以建筑图纸和房子为例,说明类和对象的关系。
- 类就相当于建筑图纸,它定义了房子的结构和特性。图纸上规定了房子应该有哪些房间、门窗的位置、墙壁的厚度等。而对象则相当于根据图纸建造出来的具体房子。每一栋房子都是一个独立的实体,具有自己独特的位置、颜色、装修风格等属性。在 Java 中,我们可以定义一个House类来表示房子的一般特征,然后通过创建House类的对象来代表具体的房子。
- 以下是用 Java 代码表示的类和对象的关系:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| // 定义 House 类 class House { // House 的属性(成员变量) String color; int rooms; // House 的方法 void showDetails() { System.out.println("这栋房子的颜色是:" + color + ",有" + rooms + "个房间。"); } } public class Main { public static void main(String[] args) { // 创建 House 类的对象 House myHouse = new House(); // 为对象的属性赋值 myHouse.color = "白色"; myHouse.rooms = 5; // 调用对象的方法 myHouse.showDetails(); } } |
- 在这个例子中,House是一个类,它定义了房子的颜色和房间数量两个属性,以及一个用于显示房子详细信息的方法。myHouse是House类的一个对象,通过为其属性赋值并调用方法,我们可以看到对象具有类中定义的属性和行为。
- 通过 "汽车" 类创建多个对象,如红色丰田车、蓝色宝马车等,展示对象的属性和方法。
- 首先定义一个Car类,代表汽车的一般特征:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| class Car { // Car 的属性(成员变量) String color; String brand; // Car 的方法 void startEngine() { System.out.println(brand + "的发动机启动了!"); } void stopEngine() { System.out.println(brand + "的发动机熄火了!"); } } |
- 然后在主方法中创建不同的汽车对象:
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public class Main { public static void main(String[] args) { // 创建红色丰田车对象 Car toyota = new Car(); toyota.color = "红色"; toyota.brand = "丰田"; // 调用对象的方法 toyota.startEngine(); toyota.stopEngine(); // 创建蓝色宝马车对象 Car bmw = new Car(); bmw.color = "蓝色"; bmw.brand = "宝马"; // 调用对象的方法 bmw.startEngine(); bmw.stopEngine(); } } |
- 在这个例子中,我们通过Car类创建了两个不同的汽车对象,每个对象都有自己独特的颜色和品牌属性,并且可以调用类中定义的启动和停止发动机的方法。这展示了对象的属性和行为是由类定义的,而每个对象又具有自己独特的属性值。
三、Java 方法的作用
(一)提高代码复用性
- 避免代码冗余,通过定义方法,在需要执行相同操作时,只需调用方法而不是复制粘贴代码。
- 在 Java 编程中,代码复用是非常重要的。通过定义方法,可以避免在多个地方编写相同的代码,减少代码冗余。例如,当需要在程序的不同部分执行相同的操作时,如打印特定格式的信息或进行某种计算,只需定义一个方法,然后在需要的地方调用该方法即可,而不是在每个地方都复制粘贴相同的代码。这样不仅可以提高开发效率,还使得代码更易于维护和修改。
- 以打印星号的需求为例,说明定义方法可以提高代码复用性。
- 假设我们有一个需求,需要在多个地方打印一行星号。如果不使用方法,可能会在每个需要的地方都手动编写打印星号的代码。如下所示:
|-------------------------------------------------------|
| System.out.println("***************"); |
- 但是,如果使用方法,我们可以定义一个方法来打印星号,然后在需要的地方调用这个方法。如下所示:
|------------------------------------------------------------------------------------|
| public void printStars() { System.out.println("***************"); } |
- 然后在需要打印星号的地方调用这个方法:
|---------------|
| printStars(); |
- 这样,当需要修改打印星号的方式时,只需要在方法中进行修改,而不需要在每个调用的地方都进行修改,提高了代码的可维护性和复用性。
(二)封装与模块化
- 封装将对象的数据和操作结合在一起,增强代码安全性和模块化。
- 封装是面向对象编程的重要特性之一。在 Java 中,封装通过访问修饰符(如 private、public、protected)将对象的内部状态(数据成员)和行为(方法)封装在一起,只对外提供有限的接口。这样可以隐藏对象的内部实现细节,增强代码的安全性和模块化。
- 例如,一个表示用户的类可能包含用户的姓名、年龄、性别等私有属性,以及一些用于获取和设置这些属性的公共方法。外部代码只能通过这些公共方法来访问和修改用户的属性,而不能直接访问私有属性。这样可以防止外部代码意外地修改用户的内部状态,提高了代码的安全性。
- 同时,封装也使得代码更加模块化。每个类都可以看作是一个独立的模块,只负责自己的内部实现,对外提供清晰的接口。这样可以降低代码的耦合度,提高代码的可维护性和可扩展性。
- 模块化将程序分解成多个方法,使代码更清晰、易于理解和维护。
- 模块化是将程序分解成多个独立的模块,每个模块负责一个特定的功能。在 Java 中,模块化可以通过将程序分解成多个方法来实现。每个方法都专注于完成一个特定的任务,使得代码更加清晰、易于理解和维护。
- 例如,一个复杂的业务逻辑可以被分解成多个方法,每个方法负责一个特定的步骤。这样,当需要修改某个步骤时,只需要修改对应的方法,而不会影响其他部分的代码。同时,其他开发人员也可以更容易地理解代码的结构和功能,提高了团队协作的效率。
(三)提高可读性与参数化
- 为方法命名可以提高代码可读性,清晰表达方法功能。
- 为方法命名是提高代码可读性的重要手段之一。一个好的方法名应该能够清晰地表达方法的功能,让其他开发人员(或未来的自己)能够快速理解代码的意图。
- 例如,一个用于计算两个数之和的方法可以命名为addNumbers,而不是一个无意义的名字如method1。这样,当其他开发人员看到这个方法名时,就能够立即知道这个方法的作用是计算两个数的和。
- 遵循命名规范也可以提高代码的可读性。例如,在 Java 中,方法名通常使用小驼峰命名法,即第一个单词小写,后面的每个单词首字母大写。这样可以使方法名更加清晰易读。
- 方法可以接受参数,处理不同数据或执行不同操作,提高代码灵活性。
- 方法可以接受参数,这使得它们能够处理不同的数据或执行不同的操作,提高了代码的灵活性。
- 例如,一个用于打印信息的方法可以接受一个字符串参数,这样就可以打印不同的信息。如下所示:
|---------------------------------------------------------------------------|
| public void printMessage(String message) { System.out.println(message); } |
- 然后在需要打印信息的地方调用这个方法,并传递不同的参数:
|----------------------------------------------------------------|
| printMessage("Hello World!"); printMessage("This is a test."); |
- 方法也可以接受多个参数,以实现更复杂的功能。例如,一个用于计算两个数之和的方法可以接受两个整数参数,然后返回它们的和。如下所示:
|-------------------------------------------------------------------|
| public int addNumbers(int num1, int num2) { return num1 + num2; } |
- 然后在需要计算两个数之和的地方调用这个方法,并传递不同的参数:
|------------------------------------------------------------------------------|
| int result = addNumbers(5, 10); System.out.println("The sum is: " + result); |
- 这样,通过接受不同的参数,方法可以处理不同的数据或执行不同的操作,提高了代码的灵活性和可重用性。
四、Java 基础的其他方面
- Java 基础课程还包括阵列、面向对象、异常、集合、IO 流、多线程等内容。
- Java 基础课程涵盖的内容十分丰富,为开发者提供了全面的编程工具和方法。除了前面已经介绍的部分,这些内容相互配合,使得 Java 编程更加高效和灵活。
- 介绍 ArrayList、Vector、LinkedList 的存储性能和特性,以及 HashMap 和 Hashtable 的区别等。
- ArrayList、Vector 和 LinkedList 都是 Java 中常用的集合类,它们在存储性能和特性上有所不同。
- ArrayList 是基于动态数组实现的列表,允许快速随机访问元素。在列表末尾插入元素的时间复杂度是 O (1),但在列表中间或开头插入元素需要移动其他元素,时间复杂度为 O (n)。查询性能通过索引查询元素的时间复杂度是 O (1)。ArrayList 不是线程安全的,如果在多线程环境下使用,需要额外的同步措施。
- Vector 与 ArrayList 类似,也是基于动态数组实现的列表,但它提供了线程安全的功能。Vector 的所有方法都是同步的,这使得它在多线程环境下更安全,但也带来了性能上的开销。在存储性能上,与 ArrayList 类似,Vector 在列表末尾插入和删除元素的时间复杂度是 O (1),但在列表中间或开头插入和删除元素的时间复杂度为 O (n)。查询性能也是 O (1)。
- LinkedList 是一个基于双向链表实现的列表,允许在列表的任意位置高效地插入和删除元素。在 LinkedList 的开头或结尾插入元素的时间复杂度是 O (1),在列表中间插入元素的时间复杂度是 O (n)。删除特定位置元素的时间复杂度也是 O (n),但删除开头或结尾的元素是 O (1)。通过索引查询 LinkedList 中元素的时间复杂度是 O (n),因为需要从头或尾开始遍历链表。
- HashMap 和 Hashtable 都是用于实现基于键值对的映射数据结构的类,它们的区别主要有以下几点:
- 线程安全性:Hashtable 是线程安全的,它的方法都是同步的。而 HashMap 则不是线程安全的,如果多个线程同时访问一个 HashMap 实例,那么可能会出现竞态条件导致数据不一致。
- null 值的处理:Hashtable 不允许键或值为 null,否则会抛出 NullPointerException 异常。而 HashMap 则允许键或值为 null,因为它使用了一个特殊的 null 键和 null 值来处理。
- 迭代器的顺序:HashMap 的迭代器不保证遍历元素的顺序,因为 HashMap 内部使用了哈希算法来存储键值对,元素的顺序是不固定的。而 Hashtable 的迭代器则保证遍历元素的顺序是按照插入的顺序,因为 Hashtable 内部使用了一个双向链表来存储键值对。
- 哈希计算方法不同:HashMap 计算 hash 对 key 的 hashcode 进行了二次 hash,以获得更好的散列值,然后对 table 数组长度取模。Hashtable 计算 hash 是直接使用 key 的 hashcode 对 table 数组的长度直接进行取模。
- 初始化容量不同:HashMap 的初始容量为 16,Hashtable 初始容量为 11。两者的负载因子默认都是 0.75。
- 扩容机制不同:已用容量 > 总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍。Hashtable 扩容规则为当前容量翻倍 +1。
- 支持的遍历种类不同:HashMap 只支持 Iterator 遍历。Hashtable 支持 Iterator 和 Enumeration 两种方式遍历。
- 迭代器不同:HashMap 的迭代器 (Iterator) 是 fail-fast 迭代器,当有其它线程改变了 HashMap 的结构(增加或者移除元素),将会抛出 ConcurrentModificationException,但迭代器本身的 remove () 方法移除元素则不会抛出 ConcurrentModificationException 异常。Hashtable 的 enumerator 迭代器不是 fail-fast 的。
- 添加 key-value 的 hash 值算法不同:HashMap 添加元素时,是使用自定义的哈希算法。Hashtable 添加元素时,是直接采用 key 的 hashCode ()。
- 部分 API 不同:HashMap 不支持 contains (Object value) 方法,没有重写 toString () 方法。Hashtable 支持 contains (Object value) 方法,而且重写了 toString () 方法。
- 性能不同:由于 HashMap 不是同步的 Map,因此在性能方面它比 Hashtable 更快,更好,实际上,它比 Hashtable 使用更少的内存。虽然它们实际上是相同的,但 Hashtable 比 HashMap 慢一点,但比同步的 HashMap 快一点。
- ArrayList、Vector 和 LinkedList 都是 Java 中常用的集合类,它们在存储性能和特性上有所不同。