目录
1.简述面向对象的三大特征。
2.简述同步机制的作用。有哪些方式可以实现线程同步?
3.简述方法重写与方法重载的区别。
4.简述 Statement 接口和 PreparedStatement 接口的区别。
5.简述抽象类与接口之间的区别。
6.什么是流? Java 中流的分类情况如何?字符流与字节流有什么区别和联系?
7.简述 Java 中异常处理的机制。
8.简述8种基本数据类型。
9.简述 Socket 的概念和作用。如何使用 Socket 实现通信?
10.简述用 abstract 修饰符修饰的类和方法有什么特点?
11.Java 语言中定义的三个标准输入输出流是什么?分别对应什么设备?
12.简述ArrayList 集合和数组的区别。
13.简述 Error 和 Exception 的区别?
14.什么是JDBC? 简述 JDBC 编程的开发步骤。
15.简述 this 与 super 关键字的区别。
16.简述流式Socket 的通信机制。它的最大特点是什么?为什么可以实现无差错通信?
17.简述线程生命周期的5种状态。
18.简述创建多线程的几种方式和它们的区别。
19.简述 InputStream 、OutputStream 、Reader 和 Writer 四个类在功能上有何异同?
20.用 Java 实现流式 Socket 通信,需要使用哪些类?它们是如何定义的?应怎样使用?
封装、继承、多态。
封装(Encapsulation):
封装是将数据(属性)和操作数据的方法(方法或函数)捆绑在一起的概念。
通过封装,对象的内部细节对外部是隐藏的,只有通过对象提供的接口(方法)才能访问和操作对象的状态。
封装提高了代码的安全性和可维护性,使得对象的实现细节可以独立变化而不影响其他部分的代码。
继承(Inheritance):
继承是一种机制,允许一个类(子类/派生类)基于另一个类(父类/基类)的定义来构建。
子类可以继承父类的属性和方法,同时可以新增或修改这些属性和方法,实现了代码的重用性和扩展性。
继承使得对象之间存在层次关系,提高了代码的抽象性和灵活性。
多态(Polymorphism):
多态是指同一个操作作用于不同的对象,可以有不同的行为。
多态分为编译时多态(静态多态,例如方法重载)和运行时多态(动态多态,例如方法重写)。
运行时多态通过方法的重写和接口的实现来实现,使得不同类型的对象可以以相同的方式被操作。
多态提高了代码的灵活性和可扩展性,使得程序更容易适应变化和扩展。
同步机制的作用:同步机制的作用是协调多个线程的执行,以确保它们按照预期的顺序和逻辑执行,从而避免因并发访问共享资源而引发的问题,如竞态条件、死锁、和数据不一致等。同步机制的目标是实现线程之间的协同工作,确保程序的正确性和稳定性。
实现线程同步的方式:
互斥锁(Mutex):
通过在关键代码段前后加锁,确保同一时刻只有一个线程可以执行该段代码,从而防止多个线程同时访问共享资源。
互斥锁提供了一种排他性的访问机制,但过度使用可能导致性能问题和死锁。
信号量(Semaphore):
信号量是一种更为通用的同步机制,不仅可以实现互斥,还可以控制同时访问共享资源的线程数量。
信号量维护一个计数器,线程在访问共享资源前必须获取信号量,访问完成后释放信号量。
条件变量(Condition Variable):
条件变量用于在多个线程之间建立一个先行条件,当某个条件满足时,线程可以继续执行,否则进入等待状态。
通常与互斥锁一起使用,以确保在条件判断和线程等待期间不会发生冲突。
读写锁(Read-Write Lock):
读写锁允许多个线程同时读取共享资源,但在写入时需要互斥访问,以提高读操作的并发性。
读写锁适用于读操作远远多于写操作的场景。
自旋锁(Spin Lock):
自旋锁是一种忙等的同步机制,线程在获取锁失败时不会阻塞,而是一直在循环中尝试获取锁。
适用于锁的持有时间很短的情况,避免了线程阻塞和唤醒的开销。
原子操作:
原子操作是不可中断的基本操作,可以确保在多线程环境中的原子性。
常见的原子操作包括原子赋值、原子递增和原子比较交换等。
方法重写(Method Overriding):
定义: 方法重写指的是在子类中重新实现(覆盖)父类中已有的方法,以便子类能够提供特定于自身需要的实现。
特点: 在方法重写中,子类中的方法与父类中的方法有相同的方法签名(方法名、参数列表和返回类型),并且子类中的方法不能比父类中的方法访问级别更严格。
目的: 主要用于实现多态,即通过父类的引用调用子类的方法时,根据实际对象的类型来执行相应的方法。
class Animal {
void makeSound() {
System.out.println("Some generic sound");
}
}
class Dog extends Animal {
// Method overriding
void makeSound() {
System.out.println("Bark");
}
}
方法重载(Method Overloading):
定义: 方法重载指的是在同一个类中可以定义多个方法,它们有相同的方法名但具有不同的参数列表(个数、类型或顺序)。
特点: 在方法重载中,方法的返回类型可以相同也可以不同。
目的: 主要用于提供一种方便的方式来定义功能类似但参数不同的多个方法。
class Calculator {
// Method overloading
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}
Statement 接口:
使用方式: Statement
是一个简单的SQL语句执行接口。每次执行SQL语句时,都需要将完整的SQL语句作为字符串传递给数据库,数据库会对该语句进行编译和执行。
SQL 注入风险: 因为SQL语句是直接拼接在代码中的,存在SQL注入的风险。如果用户输入的数据未经过充分验证或转义,可能导致安全问题。
性能: 在多次执行相似的SQL语句时,Statement
的性能相对较低,因为每次都需要重新编译SQL语句。
PreparedStatement 接口:
使用方式: PreparedStatement
是 Statement
的子接口。它使用预编译的SQL语句,允许在执行之前将参数插入到SQL语句中。这样可以防止SQL注入,并提高执行效率。
SQL 注入风险: 由于参数通过占位符的方式传递,而不是直接拼接在SQL语句中,可以有效防止SQL注入攻击。
性能: PreparedStatement
在多次执行相似的SQL语句时,性能相对较高,因为SQL语句已经被预编译,只需传递参数进行执行。
1. 定义:
抽象类: 抽象类是一个可以包含抽象方法的类,用 abstract
关键字声明。抽象类可以包含具体的方法,也可以包含属性。无法实例化抽象类,必须被子类继承并实现其抽象方法。
接口: 接口是一种完全抽象的类型,所有方法都是抽象的,而且接口不能包含字段。类通过实现接口来提供接口定义的所有方法的具体实现。
2. 继承:
抽象类: 一个类只能继承一个抽象类,使用 extends
关键字。
接口: 一个类可以实现多个接口,使用 implements
关键字。
3. 构造器:
抽象类: 抽象类可以有构造器,且构造器可以包含参数。
接口: 接口不能有构造器,因为接口不能被实例化。
4. 访问修饰符:
抽象类: 可以有访问修饰符(public、protected、default、private),允许定义实例变量,也可以有构造方法。
接口: 默认为 public,并且只能包含常量和抽象方法,不能有实例变量和构造方法(Java 8 引入默认方法和静态方法,但这不改变接口不能有实例变量和构造方法的基本事实)。
5. 实现细节:
抽象类: 可以包含非抽象方法,提供一些默认的实现。子类可以选择性地覆盖这些方法。
接口: 所有的方法都是抽象的,没有提供任何实现。实现接口的类必须提供接口中定义的所有方法的具体实现。
6. 多态性:
抽象类: 支持单继承,可以通过父类引用指向子类对象实现多态。
接口: 支持多继承,一个类可以实现多个接口,从而具有多个接口的特性。
流是一组有序的数据序列,根据操作类型可以划分为输入流和输出流。输入\输出流提供了一条通道,可以通过这条通道把源文件中的字节序列送到目的地。
Java 中流的分类:
按照处理的数据单元划分:
字节流(Byte Stream): 处理的是字节数据,主要用于处理二进制数据(如图片、音频、视频等)或字符数据。
字符流(Character Stream): 处理的是字符数据,主要用于处理文本数据。字符流使用 Unicode,并且会自动处理字符集的转换。
按照流的流向划分:
输入流(Input Stream): 用于从数据源(如文件、网络连接、内存等)读取数据。
输出流(Output Stream): 用于向目标(如文件、网络连接、内存等)写入数据。
字符流与字节流的区别和联系:
数据单元:
字节流: 处理的数据单元是字节,适用于处理二进制数据。
字符流: 处理的数据单元是字符,适用于处理文本数据。
处理方式:
字节流: 以字节为单位进行读写,通常使用 InputStream 和 OutputStream 类。
字符流: 以字符为单位进行读写,通常使用 Reader 和 Writer 类。
字符集处理:
字节流: 不关心数据的具体内容,直接读写字节,适用于处理非文本数据。
字符流: 使用字符集处理数据,可以自动进行字符集的转换,适用于处理文本数据。
适用场景:
字节流: 适用于处理二进制文件、图像、音频等非文本数据。
字符流: 适用于处理文本文件,能够更好地处理字符编码和国际化问题。
联系:
字符流通常建立在字节流之上,字符流使用字节流来实现底层的数据传输。
InputStreamReader 和 OutputStreamWriter 类是字符流和字节流之间的适配器,它们可以将字节流转换为字符流,实现字符集的转换。
Java异常处理涉及try、catch、finally、throw、throws。
如果异常抛出后处理异常,则用try-catch-finally结构处理;如果暂时不处理异常,而是将异常抛给方法的调用者进行处理,则用throws;如果不是系统自动抛出异常对象而是根据业务逻辑需要手动抛出异常对象,则用throw关键字。
try是包裹可能存在异常的代码,catch是用来处理可能产生不同类型的异常对象。getMessage()方法用于得到有关异常事件发生的信息;printStackTrace()方法用于跟踪异常事件发生时执行堆栈的内容。
finally:捕获异常的最后一步是通过finally语句为异常处理提供一个统一的出口。无论try是否发生异常,最后finally都会被执行,finally子句常用于存放释放的资源代码。
声明抛出异常:在方法声明中包含一个throws子句,throws子句列举该方法可能引起的所有异常类型。
利用throw关键字抛出异常对象,程序执行完throw就会停止,throw后面的语句不会在执行。
原始数据类型:
整数类型:
byte
: 8 位有符号整数
short
: 16 位有符号整数
int
: 32 位有符号整数
long
: 64 位有符号整数
浮点类型:
float
: 32 位单精度浮点数
double
: 64 位双精度浮点数
字符类型:
char
: 16 位 Unicode 字符
布尔类型:
boolean
: 仅取两个值之一,true
或 false
Socket是网络上运行的程序之间双向通信链路的终结点,是TCP和UDP编辑的基础,由IP地址和端口号组成。通常用于实现Client-Server连接,Java.net包中定义的两个类:Socket和ServerSocket,分别实现双向连接的Client和Server端程序。
在 Java 中,可以使用 java.net
包提供的 Socket
类和 ServerSocket
类来实现 Socket 编程。
网络编程步骤:
建立服务器ServerSocket和客户端Socket;
打开连接到Socket的输入/输出流;
按照一定的协议对Socket进行读/写操作;
关闭相对应的资源。
抽象类:
不能被实例化: 抽象类不能直接创建实例对象,即不能使用 new
操作符来实例化抽象类。这是因为抽象类本身不完整,可能包含抽象方法,需要被子类继承和实现。
可以包含抽象方法: 抽象类可以包含抽象方法(没有具体实现),也可以包含普通的方法(有具体实现)。子类继承抽象类时必须实现其中的抽象方法。
可以包含构造方法: 抽象类可以有构造方法,但无法直接被实例化。构造方法的主要作用是初始化抽象类的成员变量。
抽象方法:
没有方法体: 抽象方法声明了方法的签名但没有提供具体的实现,即没有方法体。抽象方法以分号结束,而不是花括号。
必须在抽象类中: 抽象方法只能在抽象类中声明,而不能在普通类中声明。子类继承抽象类时必须实现其中的抽象方法。
子类实现: 任何继承自抽象类的子类都必须实现抽象类中的所有抽象方法,否则子类也必须声明为抽象类。
System.in
类型: java.io.InputStream
特性:
它是一个字节输入流,用于读取字节数据(例如,从键盘输入的原始数据)。
作为字节流,它读取的数据是基本的数据单元,即字节。
用法:
InputStream
通常用于读取字节和字节数组。
要读取字符、字符串或其他非字节数据,需配合 InputStreamReader
或其他类似的转换流。例如,new BufferedReader(new InputStreamReader(System.in))
可用于缓冲字符输入。
它通常用于处理从键盘接收的输入,或者从其他字节输入源(如文件、网络连接)读取数据。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("请输入一些文本: ");
try {
String inputLine = reader.readLine(); // 读取一行输入
System.out.println("你输入的文本是: " + inputLine);
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out
类型: java.io.PrintStream
特性:
它是一个字符输出流,专门用于打印信息到控制台。
PrintStream
继承自 OutputStream
,是一种便利的输出流,可以输出不同类型的数据,包括字符串、基本数据类型的值等。
用法:
提供了多种 print
和 println
方法,用于输出各种数据类型,如 print(String)
、print(int)
、println(Object)
等。
默认情况下,System.out
输出到控制台,但也可以重定向到其他输出目的地,如文件。
public class Main {
public static void main(String[] args) {
System.out.println("这是一条字符串");
System.out.println(123); // 输出一个整数
System.out.println(45.67); // 输出一个浮点数
System.out.println(true); // 输出一个布尔值
}
}
System.err
类型: java.io.PrintStream
特性:
类似于 System.out
,也是一个 PrintStream
实例。
主要用于输出错误信息或警告,其行为与 System.out
类似,但通常用于不同的目的。
用法:
提供与 System.out
相同的方法,例如 print
和 println
。
默认情况下,输出到控制台,但通常与普通输出分开处理,使错误或警告信息更加明显。
同样可以被重定向到不同的输出目的地。
类型和实现
数组:
数组是Java中的基础数据结构,可以存储基本数据类型或对象的固定大小的集合。
数组在声明时需要指定大小,并且一旦创建,其大小不能改变。
ArrayList:
ArrayList
是一个实现了 List
接口的动态数组类,属于Java集合框架(Java Collections Framework)的一部分。
ArrayList
可以动态地增长和缩减,不需要在创建时指定大小。
大小和可伸缩性
数组:
数组的大小是固定的。创建数组时,需要指定数组的长度,之后无法更改。
ArrayList:
ArrayList
是动态的,可以根据需要自动调整大小。当添加元素超过其当前容量时,ArrayList
会自动增加其容量。
性能和效率
数组:
数组在内存分配和访问元素方面通常比 ArrayList
更高效,因为它是静态的、连续的内存布局。
ArrayList:
ArrayList
在添加或删除元素时可能不如数组高效,特别是在列表中间进行操作时,因为这可能涉及到移动元素和可能的内存重新分配。
基本类型和泛型支持
数组:
可以存储基本数据类型(如 int
, double
等)和对象类型。
数组可以直接持有基本类型的值。
ArrayList:
只能存储对象类型,不能存储基本数据类型。
ArrayList
支持泛型,可以在编译时提供类型安全检查。
功能和易用性
数组:
功能相对简单,不支持集合框架中的方法,如排序、查找等高级操作。
ArrayList:
提供了丰富的方法和功能,如自动调整大小、提供了各种便利方法(如 add
, remove
, clear
, indexOf
等)。
概念和用途
Error:java.lang.Error
表示严重的问题,通常与Java运行时系统的内部错误或资源耗尽有关,如 OutOfMemoryError
(堆内存溢出), StackOverflowError
(栈内存溢出)等。
这些问题通常是严重到程序无法处理的,因此应用程序不应该尝试捕获或处理 Error
。
try {
// 可能会抛出OutOfMemoryError
int[] largeArray = new int[Integer.MAX_VALUE];
} catch (OutOfMemoryError e) {
e.printStackTrace();
}
Exception:java.lang.Exception
表示程序可以处理的条件,比如用户输入错误、文件未找到等。
分为两类:受检异常(Checked Exceptions)和非受检异常(Unchecked Exceptions)。
受检异常(如 IOException
, SQLException
等)必须在程序中显式处理(try-catch)或向上抛出(throws)。
非受检异常(如 RuntimeException
及其子类,例如 NullPointerException
, IllegalArgumentException
等)通常是由程序逻辑错误引起的,可以选择处理或不处理。
try {
// 可能会抛出FileNotFoundException
FileInputStream file = new FileInputStream("non_existent_file.txt");
} catch (FileNotFoundException e) {
// 这是一个可以预期和处理的异常
System.out.println("文件未找到: " + e.getMessage());
}
Error:
通常不应该被应用程序捕获或处理,因为它们指示的是不太可能恢复的严重系统级问题。
处理 Error
通常意味着解决底层系统问题,而不是通过代码处理。
Exception:
应用程序应该捕获并适当处理 Exception
,或者通过声明抛出来传递到更高的处理层级。
程序设计应考虑和处理可能的 Exception
,确保程序的健壮性和稳定性。
JDBC(Java Database Connectivity)是一个Java API,用于连接和执行对数据库的查询和更新。它提供了一种标准方法,使得Java应用程序能够独立于底层数据库访问方法地与数据库进行交互。JDBC 作为一部分的 Java SE(标准版),提供了一套标准的接口和类,例如 DriverManager
, Connection
, Statement
, ResultSet
等,允许开发者执行SQL语句并处理返回结果。
导入 JDBC 包:
在Java程序中导入 java.sql.*
包,这个包包含了需要使用的JDBC类。
加载和注册 JDBC 驱动:
加载数据库提供的JDBC驱动,以便让JDBC知道如何与数据库通信。
建立数据库连接:
使用 DriverManager
类的 getConnection()
方法来建立到数据库的连接。
创建 Statement 对象:
使用连接对象创建 Statement
用于发送SQL语句到数据库。
执行 SQL 查询或更新:
使用 Statement
对象执行SQL语句,并接收结果。
处理结果:
处理数据库返回的结果(如果是查询操作),通常通过 ResultSet
对象。
关闭连接:
最后,关闭所有资源,包括 ResultSet
, Statement
, Connection
。
简单来讲:this是调用本类中的东西,super是调用父类的。
this 关键字
用途:
this
代表当前对象的引用。
它通常用于区分成员变量和参数变量,当它们的名称相同时。
功能:
引用当前类的实例变量。
调用当前类的其他构造器(在构造器中使用 this()
)。
传递当前对象作为方法的参数。
返回当前类的实例。
示例:
public class MyClass {
private int value;
public MyClass(int value) {
this.value = value; // 使用 this 来区分成员变量和参数变量
}
public void print() {
System.out.println(this.value); // 访问当前对象的成员变量
}
}
super 关键字
用途:
super
代表当前对象的父类(超类)引用。
它主要用于访问父类的成员(变量和方法)。
功能:
引用父类的实例变量。
调用父类的方法。
调用父类的构造器(在子类构造器中使用 super()
)。
示例:
public class ParentClass {
protected int value;
public ParentClass(int value) {
this.value = value;
}
}
public class ChildClass extends ParentClass {
public ChildClass(int value) {
super(value); // 调用父类的构造器
}
public void printValue() {
System.out.println(super.value); // 访问父类的成员变量
}
}
总结:
this
是对当前对象的引用,用于访问当前对象的成员变量和方法,以及调用当前类的其他构造器。
super
是对父类对象的引用,用于访问父类的成员变量和方法,以及调用父类的构造器。
在子类中,super
可以用来引用那些在子类中被覆盖(重写)的方法或变量。
流式Socket通信是基于TCP/IP协议的一种网络通信机制。在Java中,这通常是通过 java.net.Socket
类和 java.net.ServerSocket
类来实现的。流式Socket通信的机制和特点可以如下简述:
通信机制
服务器监听:
服务器端的 ServerSocket
监听指定的端口,等待客户端的连接请求。
客户端连接请求:
客户端通过 Socket
实例尝试建立连接,指定服务器的IP地址和端口号。
连接建立:
一旦服务器接受连接请求,就会建立客户端和服务器之间的TCP连接。
数据传输:
在建立的连接上,客户端和服务器可以通过输入/输出流 (InputStream
/ OutputStream
) 进行数据的发送和接收。
数据以字节流的形式传输,可以是任意类型的数据。
连接关闭:
数据传输完成后,客户端和服务器都应关闭连接和相关的流资源。
特点
可靠性:
流式Socket的最大特点是它的可靠性。基于TCP协议,它提供了一种可靠的连接导向的通信方式。
TCP保证数据的顺序性、完整性和无差错。如果发生数据丢失或错误,TCP协议会自动请求重新发送,直到数据正确传输。
TCP协议的特性:
确认和重传机制:
TCP使用确认和重传机制来保证数据传输的可靠性。发送的每个数据包都需要被接收方确认,如果发送方没有收到确认,它会重新发送数据包。
数据包排序:
TCP保证接收到的数据包按正确的顺序组装,即使它们在网络中的传输顺序被打乱。
流量控制和拥塞控制:
TCP使用流量控制和拥塞控制机制来避免网络过载,确保网络的稳定性。
错误检测:
TCP头部包含校验和,用于检测数据在传输过程中的任何错误。
线程的生命周期中包括以下5种状态。
(1)新建状态(New):使用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新建状态。与其他Java对象一样,新建线程只分配内存空间和初始化成员变量。它会一直保持这个状态直至程序调用start()方法启动该线程。
(2)就绪状态(Runnable):当线程对象调用了start)方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待Java虚拟机(JVM)里线程调度器的调度。
(3)运行状态(Running):如果就绪状态的线程获取Java虚拟机资源,即获取时间片,就开始执行run()方法,此时线程处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
(4)阻塞状态(Blocked):如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去CPU的占用权之后,该线程就从运行状态进入阻塞状态,让其他线程有机会运行。处于阻塞状态的线程在睡眠时间已到或获得设备资源后可以重新进入就绪状态。
(5)死亡状态(Dead):run方法体执行完毕,则线程正常结束。抛出Exception或调用stop()方法终止线程运行,则线程切换到死亡状态。但不推荐使用stop方法。
1. 继承 Thread
类
方式: 通过继承 Thread
类并重写其 run()
方法来创建线程。
示例:
class MyThread extends Thread {
public void run() {
// 任务代码
}
}
MyThread t = new MyThread();
t.start();
特点:
直接使用 Thread
类。
不能再继承其他类,因为Java不支持多继承。
2.实现 Runnable
接口
方式: 实现 Runnable
接口并重写 run()
方法,然后将其实例传给 Thread
类的对象。
示例:
class MyRunnable implements Runnable {
public void run() {
// 任务代码
}
}
Thread t = new Thread(new MyRunnable());
t.start();
特点:
更灵活,因为可以继续继承其他类。
推荐的方式,因为它支持多线程共享同一资源。
3.使用 Callable
和 FutureTask
方式: 实现 Callable
接口(与 Runnable
类似,但可以返回结果和抛出异常),并通过 FutureTask
包装器来创建线程。
示例:
class MyCallable implements Callable {
public Integer call() throws Exception {
// 任务代码,并返回结果
return 123;
}
}
FutureTask future = new FutureTask<>(new MyCallable());
Thread t = new Thread(future);
t.start();
特点:
可以获取执行结果和处理异常。
通常用于需要结果的并发任务。
4.使用线程池(Executor Framework)
方式: 使用 ExecutorService
创建一个线程池,提交 Runnable
或 Callable
任务到线程池执行。
示例:
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.execute(new MyRunnable());
executor.submit(new MyCallable());
executor.shutdown();
特点:
提供了资源管理,可以重复使用线程,减少线程创建和销毁的开销。
适用于大量短期异步任务的场景。
区别
继承 vs 实现: 继承 Thread
类更简单但不够灵活;实现 Runnable
接口更灵活,支持多继承。
返回值和异常: Runnable
不返回值也不抛异常,而 Callable
可以。
资源管理: 直接创建线程(前两种方式)在大量并发时可能资源消耗大;线程池(第四种方式)可以有效管理资源,提高性能。
在Java中,InputStream
、OutputStream
、Reader
和Writer
是处理输入输出(I/O)的基础类。它们之间的功能异同主要体现在处理数据的类型(字节 vs 字符)和用途(输入 vs 输出)上。
相同点
所有这些类都是用于数据的输入和输出,它们都是Java I/O流的一部分,用于读取源数据并将其转换成程序可以使用的形式,或者将数据从程序写入到某个目标。
不同点
字节流 vs 字符流:
InputStream
/OutputStream
是字节流类,用于以字节的形式读写数据。这意味着它们主要用于处理原始二进制数据,如文件、网络数据等。
Reader
/Writer
是字符流类,用于以字符的形式读写数据。字符流是处理文本数据的首选方式,因为它们考虑了字符编码的问题。
输入流 vs 输出流:
InputStream
和 Reader
用于数据的输入,即从源(如文件、内存、网络等)读取数据到程序中。
OutputStream
和 Writer
用于数据的输出,即将数据从程序写出到目标(如文件、内存、网络等)。
具体说明
InputStream:
一个抽象类,代表字节输入流的所有类的超类。
主要用于从源读取字节数据(如文件读取)。
OutputStream:
一个抽象类,代表字节输出流的所有类的超类。
主要用于向目标写入字节数据(如文件写入)。
Reader:
一个抽象类,代表字符输入流的所有类的超类。
主要用于从源读取字符数据。由于涉及字符编码,它比字节流更适合处理文本数据。
Writer:
一个抽象类,代表字符输出流的所有类的超类。
主要用于向目标写入字符数据。同样由于涉及字符编码,它比字节流更适合处理文本数据。
在选择使用字节流还是字符流时,主要考虑处理的数据类型。如果数据是原始二进制形式,如图像、音频等,最好使用字节流。如果数据是文本,最好使用字符流,因为字符流会自动处理编码和解码,确保数据的正确性。
服务器端
ServerSocket:
用于在服务器端监听客户端的连接请求。
定义:java.net.ServerSocket
使用方法:
创建 ServerSocket
实例,指定监听的端口号。
使用 accept()
方法等待客户端的连接,该方法会阻塞直到一个连接建立。
接受连接后,accept()
方法返回一个 Socket
实例用于与客户端通信。
Socket:
代表服务器与客户端之间的通信通道。
定义:java.net.Socket
使用方法:
通过 ServerSocket
的 accept()
方法获得。
使用获取的 Socket
实例的输入输出流进行数据交换。
InputStream/OutputStream:
用于数据的接收和发送。
定义:java.io.InputStream
, java.io.OutputStream
使用方法:
通过 Socket
实例的 getInputStream()
和 getOutputStream()
方法获取。
读取输入流或向输出流写入数据。
客户端
Socket:
用于与服务器建立连接并进行通信。
定义:java.net.Socket
:创建一个流套接字并将其连接到指定主机上的指定端口号。
getInputStream()
: 获取输入流,用于接收服务器发送的数据。
getOutputStream()
: 获取输出流,用于向服务器发送数据。
使用方法:
创建 Socket
实例,指定服务器的IP地址和端口号。
使用这个 Socket
实例的输入输出流与服务器通信。
InputStream/OutputStream:
与服务器端类似,用于数据的接收和发送。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(1234); // 监听1234端口
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept(); // 接受客户端连接
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
PrintWriter writer = new PrintWriter(output, true);
String line;
while ((line = reader.readLine()) != null) { // 读取客户端发送的消息
System.out.println("客户端: " + line);
writer.println("服务器回复: " + line); // 向客户端发送回复
}
socket.close();
serverSocket.close();
}
}
import java.io.*;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 1234); // 连接到服务器
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
PrintWriter writer = new PrintWriter(output, true);
writer.println("你好,服务器!"); // 向服务器发送消息
String line = reader.readLine(); // 读取服务器的回复
System.out.println("服务器回复: " + line);
socket.close();
}
}
使用方法
在服务器端,使用 ServerSocket
监听端口,等待客户端的连接。当客户端尝试连接时,使用 accept()
方法接受连接,这将返回一个 Socket
实例,用于与客户端通信。
在客户端,创建一个 Socket
实例并连接到服务器。连接成功后,也可以通过此 Socket
实例的输入输出流与服务器通信。
无论是服务器端还是客户端,都通过 Socket
提供的 InputStream
和 OutputStream
来进行数据的接收和发送。
通信完成后,关闭相应的 Socket
和 ServerSocket
。