Java之数据结构、集合源码、File类和Io流以及网络编程
Java基础学习
数据结构
1、在底层真实存在的数据结构:数组、链表
抽象数据类型:树、栈、队列(使用数组或者是链表来构建)
集合源码⭐
ArrayList
1、ArrayList的特点
- 实现了List接口,存储有序的,可重复的一个一个的数据
- 底层使用object[]数组存储
- 线程不安全
2、ArrayList源码解析
jdk7版本:
//底层会初始化数组,数组的长度为10,object[] elementDate=new object[10];
ArrayList arr=new ArrayList<>();
arr.add("AA");//elementDate[0]="AA";
arr.add("BB");//elementDate[1]="BB";
//一旦size>length,length变为原来的1.5倍,并将原ArrayList复制到扩容的ArrayList
jdk8版本:
//底层不会初始化数组,数组的长度为0,object[] elementDate=new Object[]{};
ArrayList arr=new ArrayList<>();
arr.add("AA");//首次添加元素时,会初始化数组elementData=new Object[10];elementData[0]="AA";
arr.add("BB");//elementDate[1]="BB";
//一旦size>length,length变为原来的1.5倍,并将原ArrayList复制到扩容的ArrayList
3、在选择ArrayList时,有两种初始化方法: new ArrayList();//底层创建长度为10的数组
new ArrayList(int capacity); //底层创建指定capacity长度的数组
Vector
1、Vector的特点
- 实现了List接口,存储有序的,可重复的数据
- 底层使用Object[]数组存储
- 线程安全
2、Vector源码解析
Vector v=new Vector();//底层初始化数组,长度为10 Object[] elementData=new Object[10]
v.add("AA");//elementData[0]="AA";
v.add("BB");//elementData[1]="BB";
//当添加第11个元素时,需要扩容,默认扩容为原来的2倍
LinkedList
1、LinkedList的特点
- 实现了List接口,存储有序的,可重复的数据
- 底层使用双向链表存储
- 线程不安全
2、LinkedList源码解析
LinkedList<String> list=new LinkedList<>();//因为底层不是数组结构,不需要初始化分配数组空间
list.add("AA");//将AA封装到一个Node对象1中,list对象的属性first、last都指向此Node对象1
list.add("BB");//将BB封装到一个Node对象2中,与对象1构成一个双向链表,此时last指向Node2对象
//由于LinkedList使用的是双向链表,不考虑扩容的问题
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
HashMap⭐
1、HashMap特点
- 实现Map接口,存储一对一对的数据,可以添加null的key值和value值
- 底层使用数组+单向链表+红黑树(jdk8版本之后),jdk7版本没有使用红黑树
- 线程不安全,效率高的
2、HashMap源码解析
- JDK7
//创建对象的过程中,底层初始化数组Entry[] table=new Entry[16]
HashMap<String,Integer> map=new HashMap<>();
map.put("AA",78);//将AA和78封装到一个Entry对象中,考虑将此对象添加到table数组中

情况1:将(key1,value1)存放到数组的索引i的位置
情况2,情况3:使用单向链表的头插法,将(key1,value1)放在数组中,并指向(key2,value2)
满足下列条件,会考虑扩容,一般扩容为table.legth的2倍
(size>=threshold) && (null !=table[i]) threshold=数组的长度*加载因子(默认为0.75),
加载因子过大,增加元素的数目较大时才会扩容,节省扩容所需的时间,但后期查找和删除比较麻烦
加载因子过小,没添加几个元素就开始扩容,浪费空间
HashMap有无参构造器,也有有参构造器,其中无参构造器默认capacity为16,LOAD_FACTOR为0.75,也可以直接调用有参构造器,手动输入上面两个参数的值,其中就算手动输入的capacity不是2的倍数,经过构造器之后,也会构建大小为2的倍数的capacity
HashMap允许添加key为null的值,将此(key,value)存放到table索引0的位置,如果索引为0的位置已经有元素了且遍历该位置的链表不为null,则将null用头插法放入HashMap数组中,若索引为0的key也为null或链表中有key为null,则将新的value赋给旧的value值
详细可看:JDK7中HashMap的源码 视频171集33:49
- JDK8(以jdk1.8.0_271为例)
① 在jdk8中,当我们创建了HashMap实例以后,底层并没有初始化table数组。当首次添加(key,value)时,进行判断,如果发现table尚未初始化,则对数组进行初始化。
② 在jdk8中,HashMap底层定义了Node内部类,替换jdk7中的Entry内部类。意味着,我们创建的数组是Node[]

③ 在jdk8中,如果当前的(key,value)经过一系列判断之后,可以添加到当前的数组角标i中。如果此时角标i位置上有元素。在jdk7中是将新的(key,value)指向已有的旧的元素(头插法),而在jdk8中是旧的元素指向新的 (key,value)元素(尾插法)。 "七上八下"
④ jdk7:数组+单向链表 ;jk8:数组+单向链表 + 红黑树
3、单链表和红黑树转换时机
- 使用单向链表变为红黑树:如果数组索引i位置上的元素的个数达到8,并且数组的长度达到64时,我们就将此索引i位置上的多个元素改为使用红黑树的结构进行存储。(为什么修改呢?因为红黑树进行put()/get()/remove()操作的时间复杂度为O(logn),比单向链表的时间复杂度O(n)的好,性能更高。)
- 使用红黑树变为单向链表:当使用红黑树的索引i位置上的元素的个数低于6的时候,就会将红黑树结构退化为单向链表。
属性/字段:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量 16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量 1 << 30
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子
static final int TREEIFY_THRESHOLD = 8; //默认树化阈值8,当链表的长度达到这个值后,要考虑树化
static final int UNTREEIFY_THRESHOLD = 6;//默认反树化阈值6,当树中结点的个数达到此阈值后,要考虑变为链表
//当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
//当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容(比JDK7多的一种扩容情况)
static final int MIN_TREEIFY_CAPACITY = 64; //最小树化容量64
transient Node<K,V>[] table; //数组
transient int size; //记录有效映射关系的对数,也是Entry对象的个数
int threshold; //阈值,当size达到阈值时,考虑扩容
final float loadFactor; //加载因子,影响扩容的频率
LinkedHashMap
1、LinkedHashMap与HashMap的关系
LinkedHashMap是HashMap的子类
LinkedHashMap在HashMap数组+单向链表+红黑树的基础上,又增加了一对双向链表(Entry类继承了HashMap的Node,并添加before和after作为双向指针),记录添加的(key,value)先后顺序
2、LinkedHashMap的put()方法使用了HashMap的put方法,但重写了put方法中的NewNode()方法;


HashSet和LinkedHashSet
HashSet和LinkedHashSet的底层分别是HashMap和LinkedHashMap,添加元素相当于添加key,其中key都指向同一个value,为new Object()
当对HashSet中已经存在的元素进行修改,并使用remove()方法对最新数据进行移除时,并不会删除掉该元素,因为hash值发生改变,现set中存储的却是原hash值。
添加时,不论是新的元素还是旧元素都可以添加成功。
面试⭐:
1、ArrayList相当于对数组的常见操作的封装,对外暴露增删改查插的操作方法
2、HashMap初始值为16(查看源码),临界值是12(threshold ,使用数组的长度*加载因子)
3、HashMap为什么是2的次方?看底层代码,方便计算在数组中存放角标的位置,需要和数组的length-1进行与运算,length必须为2的n次方
4、HashMap计算索引值,在1.7使用Indexfor()方法,在1.8中进行&运算
5、哈希冲突:
解决方法:头插法、尾插法、红黑树、比较equals
6、HashMap退化是因为红黑树占用空间大,TreeNode要占据两倍的普通Node的空间
7、hashCode()与equals()生成算法、方法怎么重写?进行equals()判断使用的属性,通常也都会参与到hashCode()的计算中
IDEA自动生成hashCode()自动使用相关算法
File类和IO流
1、流的基础操作
输入流 输出流
字节流: InputStream OutputStream
字符流: Reader Writer
2、字符和字节的关系 字符流和字节流是两个单位
一个char是两个byte 一个byte占8bit=>一个字符占两个字节 一个字节8比特(二进制数)
3、new FileReader(char cbuffer);该方法会返回每次读到的字符个数 且读到的字符会存在cbuffer中,循环输出时,必须要小于返回的字符个数len,因为后面循环读到的字符会覆盖前面循环读到的字符,一但后续长度不够上一轮结果有部分会保存下来。
4、try-catch执行完后还会继续往下执行,使用finally是避免catch抛出异常,后面的语句没有执行。
当try中由语句抛出异常,该语句后面的部分将不会继续执行。
节点流(FileReader FileWriter FileInputStream FileOutputStream)
1、读写字符流数据都有一定的步骤,且关闭数据流必须要放在try-catch-finally的finally中,确保即使执行过程中出错,也会关闭数据流,而当数据输入输出数据流为空时,则不需要进行关闭。
一定要关闭流资源,为了避免内存泄漏(数据已经使用完,但gc并不会回收)
public void test4(){
//1.创建File类的对象
File srcFile=new File("hello.txt");
File destFile=new File("hello_copy.txt");
//2.创建输入输出流
FileReader fr = null;
FileWriter fw= null;
try {
fr = new FileReader(srcFile);
fw = new FileWriter(destFile);
//3.数据的读入和写出的过程
char[] cbuffer=new char[5];
int len;
while ((len=fr.read(cbuffer))!=-1){
fw.write(cbuffer,0,len);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//4.关闭流资源
try {
if (fw!=null)
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fr!=null)
fr.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
2、除了文本文件以外,其它类型的文件使用字节流,和字符流的基础操作一样,除了方法里面的参数变成byte类型,如:read(byte[] buffer)和write(byte[] buffer,0,len)
3、IDEA中默认UTF-8存储,汉字默认为3个字节,其它为1个字节,当使用字节流读取复制到另一个文件时,复制成功,而如果是读出来是到控制台,如果正好中间断开中文,则会报乱码。
4、对于字符流,只能用来处理文本文件,不能用来处理非文本文件。
对于字节流,通常是用来处理非文本文件,但是如果涉及到文本文件的复制可以使用字节流
文本文件:.txt .java .c .py等各种编程语言文件
非文本文件:.doc等支持图片的
处理流
缓冲流
1、使用缓冲流 BufferInputStream BufferOutPutStream 提高文件读写的效率 默认缓冲区大小8*1024 即8kb
缓冲流的操作是先统一写到内存缓冲区里,在关闭资源时再统一写入到文件,以此提高效率,所以文件的最后如果没有关闭,则会缺失数据,此时可以用.flush()方法做到每用缓冲流写一次都会刷新数据
节点流不关闭也不会缺失数据,因为每调用一次wirite()方法都写入到文件.
转换流
1、字节流-》字符流 解码 InputStreamReader 字符流-》字节流 编码 OutputStreamWrite

2、解码时使用的字符集必须与当初编码使用的字符集兼容;如果文件编码使用的GBK,解码是UTF-8,但文件中只有abc等英文字符,此情况不会出现乱码,因为GBK和UTF-都向下兼容了ASCII。ASCII包括英文字母,数字和一些符号总共是128个,占7位,用一个字节表示
FileInputStream fis=new FileInputStream(file1);
FileOutputStream fos=new FileOutputStream(file2);
//对应的解码,必须与原字符集相同
InputStreamReader isr=new InputStreamReader(fis,"gbk");
OutputStreamWriter osw=new OutputStreamWriter(fos,"utf8");
byte[] buffer=new byte[5];
int len;
while ((len=bis.read(buffer))!=-1){
bos.write(buffer,0,len);
}
3、在存储的文件中的字符

数据流和对象流
1、对象的序列化机制

序列化过程:写出到磁盘或通过网络传输出去的过程,使用ObjectOutputStream流实现。
反序列化过程:将文件中的数据或网络传输过来的数据还原为内存中的Java对象,使用ObjectInputStream流实现。
2、流程
- 创建File对象
- File对象作为参数,创建节点流
- 节点流对象作为参数,创建对象流,调用对象流的readObject()和writeObject()方法完成对对象流的输入和输出
- 关闭最外层对象流
//数据流写到磁盘
@Test
public void test1() throws IOException {
File file=new File("object.dat");
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(file));
Person p=new Person("enheng",12);
oos.writeObject(p);
oos.flush();
oos.close();
}
//从磁盘写到内存
@Test
public void test2() throws IOException, ClassNotFoundException {
File file=new File("object.dat");
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));
Person p=(Person) ois.readObject();
System.out.println(p);
ois.close();
}
3、自定义类要实现序列化机制,要满足

类中的属性如果声明为transient或static,则不会保存在磁盘文件上,输出结果为默认值null或0
进程与进程之间通信,客户端与客户端之间进行通信,都需要对象是可序列化的。
网络编程
InetAddress

InetAddress类没有明显的构造函数,上面两个方法为工厂方法,即一个类中的静态方法返回该类的一个实例。
InetAddress inet1= InetAddress.getByName("192.168.23.21");
System.out.println(inet1);
InetAddress inet2=InetAddress.getByName("www.atguigu.com");
System.out.println(inet2);
TCP和UDP
1、TCP的三次握手

2、四次挥手

3、Socket是Ip地址+端口号
文件通过TCP从客户端传到服务器
package com.atguigu02.tcpudp;
import org.junit.Test;
import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* className:TCPTEst
* Description:
*
* @Author 董雅洁
* @Create 2024/11/22 15:07
* @Version 1.0
*/
public class TCPTest {
@Test
//客户端 发送文件给服务器端
public void client() throws IOException {
Socket socket = null;
FileInputStream fis= null;//从磁盘读到内存 用InputStram
OutputStream os = null;
try {
//1.创建Socket
InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
int port=9090;
socket = new Socket(inetAddress, port);
//2.创建File实例、FileInputStream的实例
File file=new File("1.png");
fis = new FileInputStream(file);
//3.通过Socket,获取输出流
os = socket.getOutputStream();
//4.读写数据
byte[] buffer=new byte[1024];
int len;
while ((len=fis.read(buffer))!=-1){
os.write(buffer,0,len);
}
System.out.println("数据发送完毕");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (os!=null){
os.close();
}
if (fis!=null){
fis.close();
}
if (socket!=null){
socket.close();
}
}
}
//服务器端 接受客户端发来的文件
@Test
public void server() throws IOException {
//1.创建ServerSocket
int port=9090;
ServerSocket serverSocket = new ServerSocket(port);
//2.调用accept() 接收客户端的Socket
Socket socket = serverSocket.accept();
System.out.println("服务器已开启");
//3.通过Socket获取一个输入流
InputStream is = socket.getInputStream();
//4.创建File类的实例 FileOutputStream的实例
File file=new File("1_copy.png");
FileOutputStream fos=new FileOutputStream(file);
byte[] buffer=new byte[1024];//当内容为中文时,使用一般方式可能会乱码
//ByteArrayOutputStream bos=new ByteArrayOutputStream();
int len;
while ((len=is.read(buffer))!=-1){
fos.write(buffer,0,len);
}
fos.close();
is.close();
socket.close();
serverSocket.close();
}
}


浙公网安备 33010602011771号