多线程
-
进程(process):程序的一次执行过程或是正在运行的一个程序,是一个动态的过程,有生命周期
-
线程(thread):线程是进程的组成部分,一个进程可以有多个线程,一个线程必须有一个父进程,线程是进程的执行单元;是一个程序内部的一条执行路径
- 线程是独立运行的,它并不知道进程中是否还有其他的线程存在;所以线程的执行是抢占式的
-
线程可以拥有自己的堆栈、程序计数器、和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源
-
多线程:若一个进程同一时间并行执行多个线程,就可以说此程序支持多线程的
-
单核cpu,其实是一种假的多线程,主频高,时间单元短,感觉不出来
-
并行:多个cpu同时执行多个任务
-
并发:一个cpu同时(采用时间片,并不是真的同一时间)执行多个任务;同一时刻只能由一条指令执行,但多个进程指令被快速切换,使得在宏观上具有多个进程同时执行的效果
-
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事(并行是多个线程同时的,并发是反复切换的,同一时刻只有一个在执行)
-
并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
【线程并发:同一时间,多个线程都处在新建状态到死亡状态之间,且同一时间,只能有一个线程在运行状态】
-
进程和线程的区别:
进程是正在运行中的程序,它是一个动态的过程,线程是进程的组成部分,多个线程可以再同一个进程中并行的执行;
进程是独占系统资源的,有自己的内存空间,线程不拥有系统资源,多个线程可以共享父进程的堆和方法区等资源,但每个线程可以拥有自己的程序计数器、虚拟机栈和本地方法栈
进程的资源开销大,线程的资源开销小,线程的执行是抢占式的
多线程的优势
- 进程之间不能共享内存,但线程之间共享内存很容易
- 系统创建进程时需要分配系统资源,但创建线程代价小得多,因此使用多线程来实现多任务并发比多进程效率高
- java语言内置了多线程功能支持,而不是单纯的作为底层操作系统的操作方式,从而简化了java的多线程编程
线程的创建和启动(继承Thread)
步骤:
定义Thread类的子类,并重写Thread类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把**run()**方法体称为线程执行体
创建Thread子类的实例,即创建了线程对象
调用实例的**start()**方法来启动该线程
start()方法:①启动当前线程,②:调用当前线程的run()方法
**ps:**进行多线程编程时不要忘记了Java程序默认的主线程,main()方法的方法体就是主线程的线程执行体
//创建Thread类的匿名子类的方式来创建线程,在仅使用一次线程的情况下
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i + getName());
}
}
}.start();
实现Runnable接口创建线程类
- 定义Runnable接口的实现类,并重写其run()方法
- 创建实现类的实例,把该实例作为Thread的参数创建Thread对象,该Thread对象才是线程对象
- 调用Thread对象的start()方法启动该线程
ps: Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其run()方法
//创建Thread类的对象实际调用的如下的构造器,其参数是一个Runnable类型的
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
优势:
采用Runnable接口的方式创建的多个线程可以共享Runnable实现类的实例对象。这是因为在这种方式下,程序所创建的Runnable对象只是线程的参数,而多个线程可以共享同一个参数,所以多个线程可以共享同一个线程类(实际上应该是线程的参数类)的实例变量
开发中,实现Runnable比继承Thread更适合,①实现的方式没有单继承的局限性②实现的方式多个线程可以共享数据
ps:实际上,Thread类也实现了Runnable接口
使用 Callable 和 FutureTask 创建线程
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
//6.获取Callable中call方法的返回值
try {
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
- 实现 Callable 接口 创建对线程比实现Runnable接口创建多线程的优势:
- call()方法可以有返回值
- call() 方法可以抛出异常,被外面的操作捕获,获取异常信息
- Callable是支持泛型的
线程池
/**
*使用线程池的好处:
* 1、提高响应速度(减少创建新线程的时间)
* 2、降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
* 3、便于线程管理
* corePoolSize: 核心池的大小
* maximumPoolSize: 最大线程数
* keepAliveTime: 线程没有任务时最多存活时间
*
*/
class NumberThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(i + Thread.currentThread().getName());
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 != 0){
System.out.println(i + Thread.currentThread().getName());
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1、提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//2、执行指定的线程操作,需要提供实现了Runnable接口或Callable接口的实现类的实例对象
service.execute(new NumberThread()); //适合使用Runnable
service.execute(new NumberThread1()); //适合使用Runnable
//service.submit(Callable callable); //适合使用Callable
//3、关闭连接池
service.shutdown();
}
}
线程中常用的方法
- start(): 启动当前线程并调用当前线程的run()方法
- run(): 通常需要重写Thread类中的此方法,其方法体代表了线程所要完成的任务
- currentThread(): 静态方法,返回当前正在执行的线程对象
- getName(): 返回调用该方法的线程的名字
- setName(String name): 设置线程名字(默认情况下,主线程的名字是main,用户启动的线程名字依次为Thread-0,Thread-1…)
- static yield(): (放弃的意思) 当前线程释放当前cpu执行权,但不会阻塞,会进入就绪状态,yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,实际上,当某个线程调用了yield()方法暂停以后,只有优先级大于等于当前线程的就绪状态的线程才会获得执行机会
- join(): 把另一个线程加塞进来执行直到此线程(加塞进来的)结束才继续执行
- stop(): [已过时] 强制结束当前线程
- static sleep(long millis): 让当前线程睡眠指定的毫秒数,并进入阻塞状态,sleep()方法的定义抛出了InterruptedException
- isAlive(): 判断当前线程是否还存活(处于就绪、运行、阻塞时返回true,处于新建和死亡返回false)
- isDaemon():是否为后台线程(JVM的垃圾回收线程就是典型的后台线程)
线程的优先级
Thread类提供了setPriority()、getPriority() 方法来设置和返回指定线程的优先级
setPriority() :方法的参数是一个整数,范围是 1~10 之间,也可以使用Thread类的如下三个静态常量:
MAX_PRIORITY: 其值是10
MIN_PRIORITY: 其值是1
NORM_PRIORITY: 其值是5
高优先级的线程获得较多的执行机会,但是并不意味着高优先级的线程执行完成后才执行低优先级的线程只是概率更高
线程的生命周期
-
五个周期:
-
**新建(**New):当程序使用new关键字创建一个线程后,该线程就处于新建状态
-
**就绪(**Ready):调用start()方法之后,该线程就处于就绪状态,Java虚拟机会为其创建方法调用 栈和程序计数器(调用start ()后当前线程并没有立即执行,何时执行由底层平台控制,有随机性)
-
**运行(**Running):处于就绪状态的线程获得cpu了,就开始执行run()方法的线程执行体,则该线程处于运行状态
ps:如果只有一个cpu则永远只有一个线程处于运行状态,如果有多个cpu则多个线程可以并行(注意是并行(parallel) 执行
-
**阻塞(**Blocked):线程在运行过程中需要被中断进入阻塞状态
-
**死亡(**Dead):线程结束后就处于死亡状态 有以下三种方式结束:
run()方法或call()方法执行完成,线程正常结束
线程抛出一个未捕获的Exception或Error
直接调用该线程的stop()方法结束该线程----容易导致死锁不推荐
ps:只能对处于新建状态的线程调用start()方法,否则会报 IllegalThreadStateException
线程同步
线程安全问题: 当有多个线程并发修改某个数据就有可能造成异常
线程安全的类的特征:
-
该类的对象可以被多个线程安全的访问
-
每个线程调用该对象的任意方法后都将得到正确的结果
-
每个线程调用该对象的任意方法后,该对象状态仍保持合理状态
通过同步机制解决线程安全问题的措施:三种
①:同步代码块:
java的多线程引入同步监视器来解决,使用同步监视器的通用方法就是同步代码块:
synchronized(Obj即同步监视器){
//需要被同步的代码
}
//操作共享数据的代码即为需要被同步的代码,不能包多,也不能包少
//上述代码的含义:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定
ps: 同步监视器,俗称锁,任何一个类的对象都可以充当锁,也可以使用当前类来充当(类也是对象 obj.class)(通常推荐使用可能被并发访问的共享资源充当同步监视器;同步监视器的要求:所有的线程必须共用同一把锁(同步监视器))
逻辑:加锁–>修改–>释放锁,通过这种方式可以保证并发线程在任意时刻只有一个线程可以进入修改共享资源的代码区(也称临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性
②:同步方法:
-
同步方法就是使用synchronized关键字来修饰某个方法,则该方法就称为同步方法
-
对于使用synchronized修饰的实例方法(而不是static方法),无需显式的指定同步监视器(锁),同步方法的同步监视器是this,也就是调用该方法的对象
③:同步锁(Lock)
**同步锁(lock)**是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前先获得Lock对象。
**Lock、ReadWriteLock(读写锁)**是java 5 提供的两个根接口。
分别为Lock接口提供了ReentrantLock(可重入锁)实现类;为ReadWriteLock接口提供了ReentrantReadWriteLock实现类。
在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显示地加锁、释放锁。
-
synchronized 和 lock 的异同
- synchronized 机制在执行完成相应的代码后,自动的释放锁(自动监视器)
- lock 需要手动的启动锁(同步)和手动的释放锁(同步)
建议优先使用lock(更灵活)次之同步代码块,再次同步方法
-
死锁
不同的线程分别占用对方的锁(同步监视器)不放弃,都在等待对方的释放锁,就会发生死锁
java虚拟机没有检测,也没有采取措施来处理死锁情况,一旦出现死锁,程序不会出现异常,也不会给任何提示,只是所有线程处于阻塞状态无法继续
**避免死锁的措施:**四个
-
避免多次锁定,也就是避免加多个锁
-
具有相同的加锁顺序,如果多个线程需要对多个锁(同步监视器)进行锁定,则应该保证他们对不同锁的加锁顺序相同,比如一个线程的加锁顺序是a、b另一个线程的加锁顺序也应该是a、b
-
使用定时锁,程序调用 Lock 对象的 tryLock() 方法加锁时可指定 time 和 unit 参数,当超过指定时间后就释放锁了
-
死锁检测,这是一种依靠算法来实现死锁的预防机制
-
-
释放同步监视器(锁)的锁定
-
任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显示释放对同步监视器的锁定,
-
线程会在如下几种情况下释放对同步监视器的锁定:
1、当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
2、当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。
3、当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时,当前线程会释放同步监视器。
4、当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
在如下所示的情况下,线程不会释放同步监视器:
1、线程执行同步代码块或同步方法时,程序调用了Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
2、线程执行同步代码块时,其他线程调用了该线程的suspend()方法将线程挂起,该线程不会释放同步监视器。当然,我们应该尽量避免使用suspend()和resume()方法来控制线程。
-
线程通信
-
传统线程通信
借助Object类提供的(注意时Object类的而不是Thread类的)wait()、notify()、notifyAll() 三个方法实现,注意:这三个方法必须由同步监视器对象(锁)调用
-
wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程,wait()方法有三种参数形式,①无参,一直等待②一个参数毫秒、等待指定时间自动苏醒③二个参数,毫秒,微秒。注意:调用wait()方法的当前线程会释放锁(同步监视器)
-
notify():唤醒在此同步监视器上等待的单个线程。如果多个线程在等待,则会随机选择一个唤醒,但是依赖于具体实现的JVM(简单说jvm的实现方式不同,结果不同,不一定随机的)
-
notify():唤醒在此同步监视器上等待的所有线程
-
-
使用 Condition 控制线程通信
-
如果程序不适用 synchronized 关键字实现同步,而是直接使用Lock对象实现同步,则系统中不存在隐式的同步监视器,也就不能使用wait()\notify()、notifyAll() 方法进行线程通信了
-
当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象、却无法继续执行的线程释放Lock对象,Condtion对象也可以唤醒其他处于等待的线程。
-
Condition 将同步监视锁方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock 替代了同步方法或同步代码块,Condition替代了同步监视锁的功能。
Condition实例实质上被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象newCondition()方法即可。
Condtion类提供了如下三个方法:
-
await():类似于隐式同步监视器上的wait()方法,导致当前线程等待,直到其他线程调用该Condtion的signal ()方法或signalAll ()方法来唤醒该线程。该await方法有更多变体:long awaitNanos(long nanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline)等,可以完成更丰富的等待操作。
-
signal ():唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程。
-
signalAll():唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该该Lock对象的锁定后,才可以执行被唤醒的线程。
-
-
-
使用阻塞队列(BlockingQueue)控制线程通信
基础类库
Object 类
-
Object 类是所有类、数组、枚举类的父类
-
Object 提供了如下几个常用的方法
boolean equals(Obj obj);//比较的是地址值 Class<?> getClass();//返回该对象的运行时类 int hashCode();//返回该对象的 hashCode 值 String toString();//返回该对象的字符串表示,当程序使用System.out.println()方法输出一个对象,或者把一个对象和字符串对象进行连接运算时,系统会自动调用该对象的toString()方法返回该对象的字符串表示。 protected clone()//该方法用于克隆一个调用对象的副本
Object 类的 toString()方法返回 ”运行时类名@十六进制 hashCode 值 “ 格式的字符串表示
-
java7 新增了一个 Objects 工具类,它提供了一些工具方法来操作对象,这些方法大多是“空指针”安全的(也就是不会报NullPointerException异常)
Scanner 类(获取键盘输入)
-
使用Scanner类可以获取用户的键盘输入,它可以从文件、输入流、字符串中解析出基本类型值和字符串值
-
Scanner 中的方法:
- nextXxx : 获取下一个输入项。其中Xxx可以是 int,long 等代表基本数据类型的字符串
- hasNextXxx : 是否还有下一个输入项
默认情况下 Scanner 使用空白(包括空白、Tab空白、回车)作为多个输入项之间的分隔符,可以使用useDelimiter(String pattern)方法为Scanner设置分隔符
Scanner 提供了两个简单的方法来逐行读取
- String nextLine():返回输入源中下一行字符串
- boolean hasNextLine:返回输入源中是否还有下一行
- Scanner 的使用
//创建Scanner对象, System.in 代表标准输入,就是键盘输入 Scanner sc = new Scanner(System.in); //Scanner 还可以读取文件输入,只要在创建Scanner对象时传入一个 File 对象作为参数,就可以让 Scanner读取该文件的内容 Scanner sc = new Scanner(new File("ScannerFileTest.java")); while(sc.hasNextLine){ System.out.println(sc.nextLine); }
字符串相关的类:String
补充:字符串常量池在方法区
- String 类:
- **【非常重要:不可变性】String类是不可变类,即一旦一个String对象被创建后,包含在此对象内的字符序列是不可改变的,直到此对象被摧毁 **
- String类声明为final,不可被继承
- String类实现了Serializable接口:表示字符串支持序列化
实现了Comparable接口:表示String可以比较大小
- java8 及以前 String类定义了final char[] value 用于存储字符串数据(由于String使用final 修饰的char型数组存储字符串,所以String声明的字符串对象被创建后,包含在此对象内的字符序列是不可改变的,直到此对象被摧毁),简称不可变性。
- java9改进了字符串(包括String,StringBuffer,StringBuilder)的实现,在java9之前使用char[]数组保存字符,因此字符串的每个字符占两个字节,而java9的字符串采用 byte[] 数组再加一个encoding-flag 字段来保存字符,因此每个字符串字符只占一个字节
- 通过字面量的方式(不同与new)给字符串赋值,此时字符串值声明在字符串常量池中
- 字符串常量池中是不会存储相同的字符串的(当采用字面量的方式赋值给一个字符串时会自动检查常量池有没有此字面量,如果有就直接把此字面量的地址赋给字符串对象)
String的两种实例化方式的区别:
String s1 = "abc"; //此时s1指向的数据是声明在方法区中的字符串常量池中
String s2 = new String("abc");//由于是使用new关键字创建的对象,所以s2指向的数据声明在堆空间中
/*
结论:
1.常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量。
2.只要其中有一个是变量,结果就在堆中。(如果给变量加上final修饰就变成了常量此时就适用第1条 )
3.如果拼接的结果调用intern()方法,返回值就在常量池中
*/
@Test
public void test3(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
s1 += "hadoop";//此时也是设计的变量操作适用于第二条
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//false
System.out.println(s3 == s7);//false
System.out.println(s5 == s6);//false
System.out.println(s5 == s7);//false
System.out.println(s6 == s7);//false
String s8 = s6.intern();//返回值得到的s8使用常量池中已经存在的“javaEEhadoop”
System.out.println(s3 == s8);//true
final String s22 = "hadoop";
String s44 = "javaEE" + s22;
System.out.println(s3 == s44);//true. fianl修饰的是常量所以s22是常量适用于第一条
}
String类中的常用方法:
-
int compareTo(String anotherString): 比较两个字符串的大小(可用于字符串排序)
-
boolean contains(CharSequence s)://判断是否包含某字符串,当且仅当此字符串包含指定的 char 值序列时,返回 true
-
boolean matches(String regex)://告知此字符串是否匹配给定的正则表达式。(可以用于验证手机号位数)
int compareTo (String anotherString)//比较两个字符串的大小,如果两个字符串的字符序列相等, 则返回0; // 不相等时,从两个字符串第0个字符开始比较,返回第一个不相等的字符的差(ASCII 码相减), // 另一种情况,较长字符串的前面部分恰好是较短字符串,则返回他们的长度差 boolean contains(CharSequence s)://判断是否包含某字符串,当且仅当此字符串包含指定的 char 值序列时,返回 true boolean matches(String regex)://告知此字符串是否匹配给定的正则表达式(可以用于验证手机号位数)
int length(): //返回字符串的长度: return value.length
char charAt(int index): //返回某索引处的字符return value[index]
boolean isEmpty(): //判断是否是空字符串:return value.length == 0
String toLowerCase()://使用默认语言环境,将 String 中的所有字符转换为小写
String toUpperCase()://使用默认语言环境,将 String 中的所有字符转换为大写
String trim(): // 返回字符串的副本(不可变性),忽略前导空白和尾部空白
boolean equals(Object obj)://比较字符串的内容是否相同
boolean equalsIgnoreCase(String anotherString)://与equals方法类似,忽略大小写
String concat(String str): //将指定字符串连接到此字符串的结尾。 等价于用“+”
int compareTo(String anotherString): //比较两个字符串的大小(可用于字符串排序)
String substring(int beginIndex)://返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串。
String substring(int beginIndex, int endIndex) ://返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。
String s1 = " Hello World ";
s1.length();//返回字符串长度
s1.charAt(2);//返回某索引处字符
s1.isEmpty();//判断当前字符数组是否为空
String s2 = s1.toLowerCase();//字符串中所有字符转换成小写
String s3 = s1.toUpperCase();//字符串中所有字符转换成大写
String s4 = s1.trim();//去除首尾空格,中间的空格不动
s1.equalsIgnoreCase(s4);//忽略大小写的情况下比较字符串内容是否相等
s1.concat(s2); //连接两个字符串和 + 的功能相同
s1.compareTo(s2);//比较两个字符串的大小,如果两个字符串的字符序列相等,则返回0;
// 不相等时,从两个字符串第0个字符开始比较,返回第一个不相等的字符的差 (ASCII码相减),
// 另一种情况,较长字符串的前面部分恰好是较短字符串,则返回他们的长度差
s1.substring(2);//一个参数(int beginIndex)取索引开始位置往后的字符串
s1.substring(2,4);//两个参数(int beginIndex,int endIndex)取指定索引区间的字符串,
// 注意是半开半闭区间包含beginIndex,不包含endIndex
还有一些常用的:
boolean endsWith(String suffix)://测试此字符串是否以指定的后缀结束
boolean startsWith(String prefix)://测试此字符串是否以指定的前缀开始
boolean startsWith(String prefix, int toffset)://测试此字符串从指定索引开始的子字符串是否以指定 前缀开始
boolean contains(CharSequence s)://判断是否包含某字符串,当且仅当此字符串包含指定的 char 值序 列时,返回 true
int indexOf(String str)://返回指定子字符串在此字符串中第一次出现处的索引
int indexOf(String str, int fromIndex)://返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
int lastIndexOf(String str)://返回指定子字符串在此字符串中最右边出现处的索引
int lastIndexOf(String str, int fromIndex)://返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
//注:indexOf和lastIndexOf方法如果未找到都是返回-1
//替换:
String replace(char oldChar, char newChar)://返回一个新的字符串,它是通过用 newChar 替换此 字符串中出现的所有 oldChar 得到的。
String replace(CharSequence target, CharSequence replacement)//:使用指定的字面值替换序列 替换此字符串所有匹配字面值目标序列的子字符串。
String replaceAll(String regex, String replacement)://使用给定的 replacement 替换此字符串 所有匹配给定的正则表达式的子字符串。
String replaceFirst(String regex, String replacement)://使用给定的 replacement 替换此字 符串匹配给定的正则表达式的第一个子字符串。
//匹配:
boolean matches(String regex)://告知此字符串是否匹配给定的正则表达式。(可以用于验证手机号位数)
//切片:
String[] split(String regex)://根据给定正则表达式的匹配拆分此字符串。
String[] split(String regex, int limit)://根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。
String类与基本数据类型、包装类之间的转换
- String —> 基本数据类型、包装类:
- 调用包装类的 parseXxx(String str) 静态方法(除Character之外所有包装类均提供了该方法);
- 利用包装类提供的 valueOf(String s) 静态方法
- 基本数据类型、包装类 —> String:
- 调用String类的 valueOf() 方法
String与char[] 之间的转换
- String —> char[]:
- 调用String的 toCharArray() 方法
-
char[] —> String
-
调用String(char[] value)的构造器 :
-
char[] chars = new char[]{'h','e','l','l','o'}; String str = new String(chars);
-
String与 byte[] 之间的转换
- String —> byte[] :(编码)
-
调用String类的 getBytes() 方法
String s = "abc123"; byte[] bytes = s.getBytes(); System.out.println(Arrays.toString(bytes));//输出的是ASCII码[97, 98, 99, 49, 50, 51]
-
byte[] —> String : (解码)
-
调用 String(byte[] bytes) 构造器
String s = "abc123"; byte[] bytes = s.getBytes(); System.out.println(Arrays.toString(bytes)); String s9 = new String(bytes); System.out.println(s9);//输出abc123
-
StringBuffer和StringBuilder
-
String 和 StringBuffer、StringBuilder
-
String : 不可变的字符序列
-
StringBuffer: 可变的字符序列,线程安全的,效率比较低(多线程时使用)
-
StringBuilder:(jdk1.5新增)可变的字符序列,线程不安全的,效率高(单线程使用)
-
-
通常情况下,如果要创建一个内容可变的字符串对象,则应该优先考虑StringBuilder类(不涉及多线程的话)
-
StringBuilder、StringBuffer有两个属性:length 和 capacity【容量】,其中length属性表示其包含的字符序列的长度。与String对象的length不同的是,StringBuilder、StringBuffer的length是可以改变的,可以通过length()、setLength(int len), 方法来访问和修改字符序列的长度。capacity属性表示的是StringBuilder、StringBuffer的容量,capacity通常比length大
-
String str = new String();// char[] value = new char[0]; String str1 = new String("abc");//char[] value = new char[]{'a','b','c'}; StringBuffer sb1 = new StringBuffer();//char[] value = new char[ capacity: 16],底层创建了一个容量是16的char型数组 StringBuffer sb2 = new StringBuffer("abc");//char[] value = new char[sb2.length + 16],在字符序列的长度的基础上加16
扩容问题:(当添加的字符超过了capacity【容量】的长度后)
扩容为原来容量长度的2倍 + 2,如果要添加的字符长度特别长就把该字符长度+原来的字符长度作为新的capacity
-
在开发当中建议使用 StringBuffer(int capacity) 的构造器,来指定容量【capacity】
-
StringBuffer中常用的方法:
StringBuffer append(xxx)://提供了很多的append()方法,用于进行字符串拼接,拼接完原来的字符串就不存在了 StringBuffer delete(int start,int end)://删除指定位置的内容 StringBuffer replace(int start, int end, String str)://把[start,end)位置替换为str StringBuffer insert(int offset, xxx)://在指定位置插入xxx StringBuffer reverse() ://把当前字符序列逆转 int indexOf(String str) String substring(int start,int end)://返回一个从start开始到end索引结束的左闭右开区间的子字符串 int length() char charAt(int n ) void setCharAt(int n ,char ch)
增:append():支持方法链的形式调用
删:delete(int start, int end)
改:setCharAt(int n, char ch)/ replace(int start, int end, String str)
查:charAt(int n)
插:insert(int offset, xxx)
长度:length()
遍历:for() + charAt()
-
String — > StringBuffer、StringBuilder: 调用StringBuffer、StringBuilder的构造器
-
StringBuffer、StringBuilder —> String:
- 调用String 构造器
- 调用StringBuffer、StringBuilder的 toString()方法
String s1 = new String("abc"); StringBuffer sb = new StringBuffer(s1); String s2 = new String(sb); String s3 = sb.toString();
日期、时间类
一、java8之前的日期、时间类
- java.lang.System类 System类提供的public static long **currentTimeMillis()**用来返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差。 此方法适于计算时间差。
-
Date类
java提供了Date类来处理日期、时间(此处指的是java.util包下的Date类,不是java.sql包下的Date类)
Date类的大部分构造器、方法都已过时,不推荐使用,总的来说,Date是一个设计相当糟糕的类,因此Java官方推荐尽量少用Date类的构造器和方法,如果要对日期、时间进行加减运算,或获取指定时间的年、月、日、时、分、秒信息,可使用Calendar【日历】工具类
-
Date的构造器(6个,4个不推荐使用了)
Date(): 生成一个当前日期时间的Date对象,该构造器底层调用System类的currentTimeMillis方法
Date(long date): 根据指定的long型整数生成一个Date对象,表示long型参数和1970.1.1.00.00的差
-
方法
toString():返回当前日期时间
long getTime(): 返回该时间对应的long型整数(毫秒为单位,和1970.1.1.00.00的差)
void setTime(long time):设置该Date对象的时间
boolean after(Date when):测试该日期是否在指定日期when之后
-
-
如何将java.util.Date对象转换为java.sql.Date对象
java.util.Date date1 = new java.util.Date();
java.sql.Date date2 = new java.sql.Date(date1.getTime());//把date1的毫秒数拿过来
- **Calendar类**(日历)【抽象类】
1. 实例化Calendar类
1. 调用Calendar的静态方法getInstance()获取Calendar对象,(实际上getInstance方法也是调用的Calendar子类GregorianCalendar的构造器创建的子类的实例)
2. 实例化Calendar的子类
2. 常用方法
```java
@Test
public void test1(){
//实例化
Calendar calendar = Calendar.getInstance();
//System.out.println(calendar.getClass());
//常用方法
//get()
int day = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(day);//20
System.out.println(calendar.get(Calendar.DAY_OF_YEAR));//110
//set()
calendar.set(Calendar.DAY_OF_MONTH,22);
System.out.println(calendar.get(Calendar.DAY_OF_MONTH));//22
//add()
calendar.add(Calendar.DAY_OF_MONTH,3);
System.out.println(calendar.get(Calendar.DAY_OF_MONTH));//25
//getTime() Calendar ---> Date
System.out.println(calendar.getTime());//Sun Apr 25 11:09:54 CST 2021
//setTime() Date ---> Calendar
Date date = new Date();
calendar.setTime(date);
System.out.println(calendar.getTime());//Tue Apr 20 11:12:02 CST 2021
}
-
SimpleDateFormat(对日期Date类的格式化和解析)
- 格式化:日期 -----> 字符串
- 解析:格式化的逆过程 字符串 -----> 日期
@Test public void testFormat() throws ParseException { //实例化SimpleDateFormat SimpleDateFormat sdf = new SimpleDateFormat(); //格式化:Date ---->String Date date = new Date(); System.out.println(date);//Tue Apr 20 09:33:00 CST 2021 String format = sdf.format(date); System.out.println(format);//21-4-20 上午9:33 //解析:String ---> Date String str = "2020-10-10 上午10:23"; Date date1 = sdf.parse(str); System.out.println(date1);//Sat Oct 10 10:23:00 CST 2020 //指定的方式格式化和解析(选用带参构造器) SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); String format1 = sdf1.format(date); System.out.println(format1);//2021-04-20 09:33:00 Date date2 = sdf1.parse(format1); System.out.println(date2); }
二、JDK8中新的日期、时间包
- java8中新增了一个 java.time 包
Java比较器(Comparable)
为了满足对两个对象排序的需求
通过实现 Comparable 接口或者实现 Comparator 接口
-
实现 Comparable 接口:(自然排序)
Comparable可以认为是一个内比较器,实现了Comparable接口的类有一个特点,就是这些 类是可以和自己比较的。
若一个类实现了Comparable接口,就意味着该类支持排序。实现了Comparable接口的类的对象的列表或数组可以通过Collections.sort或Arrays.sort进行自动排序。
此外,实现此接口的对象可以用作有序映射中的键或有序集合中的集合,无需指定比较器。
- 像String、包装类等都是实现了Comparable接口,重写了 int compareTo(T obj)方法,
- 重写compareTo方法的规则:
- 如果当前对象this大于形参对象obj,则返回正整数
- 如果当前对象this等于形参对象obj,则返回零
- 如果当前对象this小于形参对象obj,则返回负整数
- 对于自定义类,如果需要排序,就可以实现Comparable接口,重写compareTo方法
-
实现 Comparator 接口:
Comparator是比较接口,我们如果需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口),那么我们就可以建立一个“该类的比较器”来进行排序,这个“比较器”只需要实现Comparator接口即可。
【当元素的类型没实现java.lang.Comparable接口而又不方便修改代码,或者实现了java.lang.Comparable接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 的对象来排序】
-
实现Comparator接口并重写 int compare(Object o1,Object o2)方法
如果 o1 > o2 , 返回正整数
如果 o1 == o2, 返回零
如果 o1 < o2, 返回负数
-
-
Comparable和Comparator的比较
- Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。而Comparator是比较器,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。解耦了~~
- Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
- Comparable接口是 java.lang包下的 而 Comparator接口才是java.util包下的。(由此后者被归类为一种工具)
- 两种方法各有优劣, 用Comparable 简单, 只要实现Comparable 接口的对象直接就成为一个可以比较的对象,但是需要修改源代码。(有侵入性)
用Comparator 的好处是不需要修改源代码, 而是另外实现一个比较器, 当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了, 并且在Comparator里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。
System类
-
系统相关的类,该类的构造器是private的,所以不能创建该类的实例,因此System提供了一些static的变量和方法
-
成员变量
System类内部包含in、out和err三个成员变量,分别代表标准输入流 (键盘输入),标准输出流(显示器)和标准错误输出流(显示器)。
-
成员方法
native long currentTimeMillis();//该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数。 void exit(int status) //该方法的作用是退出程序,其中status为0代表正常退出,非0代表异常退出,使用该方法可以在图形界面编程中实现程序的退出功能等 void gc() //该方法的作用是通知系统进行垃圾回收,至于是否立刻回收,取决于系统中垃圾回收算法的实现以及系统执行时的情况 String getProperty(String key)//该方法的作用是获得系统中属性名为key的属性对应的值。系统中常见的属性名以及属性的作用如下表所示: //java.version : java的版本 String javaVersion = System.getProperty("java.version"); System.out.println("java的version:" + javaVersion); //java的version:1.8.0_261 //java.home : java安装目录 String javaHome = System.getProperty("java.home"); System.out.println("java的home:" + javaHome); //java的home:C:\Program Files\Java\jdk1.8.0_261\jre //os.naem : 操作系统名称 String osName = System.getProperty("os.name"); System.out.println("os的name:" + osName); //os的name:Windows 10 //os.version : 操作系统的版本 String osVersion = System.getProperty("os.version"); System.out.println("os的version:" + osVersion); //os的version:10.0 //user.name : 用户名 String userName = System.getProperty("user.name"); System.out.println("user的name:" + userName); //user的name:Think //user.home : 用户的主目录 String userHome = System.getProperty("user.home"); System.out.println("user的home:" + userHome); //user的home:C:\Users\Think //user.dir : 用户的当前工作目录 String userDir = System.getProperty("user.dir"); System.out.println("user的dir:" + userDir); //user的dir:E:\zaxiang\code
Math类
-
Java提供 Math 工具类来完成更复杂的数学运算,Math类的构造器是 private 的,所以不能创建对象;Math类的所有方法都是类方法,Math类除提供了大量的静态方法之外,还提供了两个类变量:PI 和 E,它们的值分别是 Π 和 e
-
Math类一些常用方法:
Math.round(2.3);//四舍五入取整 Math.sqrt(2.3);//计算平方根 Math.pow(3,2);//计算乘方 Math.abs(-2.3);//计算绝对值 Math.max(2,3);//计算最大值(只有两个参数) Math.min(2,3);//计算最小值(两个参数) Math.random();//返回一个伪随机数(有规则的随机),该值[0.1,1.0)
Random 与 ThreadLocalRandom
-
Random 类专门用于生成一个伪随机数,它有两个构造器:一个无参构造器(默认以当前时间作为种子(形参名)),一个Random(long seed)构造器 需要传入一个long型整数种子(参数)
-
ThreadLocalRandom 类是Java7 新增的一个类,它是Random的增强版。在并发访问的环境下具有更好的安全性
-
Random 和 ThreadLocalRandom 类用法基本相似,它们提供了更多的方式来生成随机数,可以生成浮点类型的伪随机数,整数类型的伪随机数,还可以指定生成随机数的范围()
//创建Random对象 Random random = new Random(); //生成一个处于int整数取值范围的伪随机数 System.out.println(random.nextInt()); //生成一个 0-10 之间的伪随机数 System.out.println(random.nextInt(10));
-
Random random1 = new Random(50); System.out.println(random1.nextInt());//-1160871061 System.out.println(random1.nextDouble());//0.5978920402252371 Random random2 = new Random(50); System.out.println(random2.nextInt());//-1160871061 System.out.println(random2.nextDouble());//0.5978920402252371 Random random1 = new Random(50); System.out.println(random1.nextInt()); //-1160871061 System.out.println(random1.nextDouble());//0.5978920402252371 Random random2 = new Random(50); System.out.println(random2.nextDouble());//0.7297136425657874 System.out.println(random2.nextInt());//-1657178909
从上面的运行结果可以看出,只要两个Random对象的种子相同,并且方法的调用顺序也相同,它们就会产生相同的数字序列【Random产生的数字并不是真正随机的,而是一种伪随机】
为了避免两个Random对象产生相同的数字序列,通常推荐使用当前时间作为Random对象的种子
Random rand = new Random(System.currentTimeMillis);
BigDecimal 类
1、BigDecimal介绍
Java中提供了大数字(超过16位有效位)的操作类,即 java.math.BinInteger 类和 java.math.BigDecimal 类,用于高精度计算.其中 BigInteger 类是针对大整数的处理类,而 BigDecimal 类则是针对大浮点数的处理类.BigDecimal 类的实现用到了 BigInteger类,不同的是 BigDecimal 加入了小数的概念.float和Double只能用来做科学计算或者是工程计算;在商业计算中,对数字精度要求较高,必须使用 BigInteger 类和 BigDecimal 类,它支持任何精度的定点数,可以用它来精确计算货币值。
由于在运算的时候,float类型和double很容易丢失精度,所以一般不用来做计算货币。
2、BigDecimal构造方法
有三种类型的构造方法:
1、方法一
BigDecimal BigDecimal(double d); //不允许使用
2、方法二
BigDecimal BigDecimal(String s); //常用,推荐使用
3、方法三
static BigDecimal valueOf(double d); //常用,推荐使用。valueOf()方法中也是调用的Double的toString()方法
1.不推荐使用BigDecimal(double val)构造器,因为使用该构造器时有一定的不可预知性,当程序使用new BigDecimal(0.1)创建一个BigDecimal对象时,它的值并不是0.1,实际上是一个近似0.1的数。
2.建议优先使用基于String的构造器,使用BigDecimal(String val)构造器时可以预知的,写入new BigDecimal(“0.1”)将创建一个恰好等于0.1的BigDecimal。
3.如果必须使用double浮点数作为BigDecimal构造器的参数时,不要使用double作为参数,而应该通过BigDecimal.valueOf(double value)静态方法来创建对象。
3、BigDecimal类成员方法
public BigDecimal add(BigDecimal augend)://加
public BigDecimal subtract(BigDecimal subtrahend)://减
public BigDecimal multiply(BigDecimal multiplicand)://乘
public BigDecimal divide(BigDecimal divisor)://除
public BigDecimal divide(BigDecimal divisor,int scale, int roundingMode)://商,几位小数,舍取模式
正则表达式
集合
集合概述
-
Java 集合大致可分为: Set、List,Queue 和 Map 四种体系
- 数组的长度不可变化,为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java提供了集合类,所有的集合类都位于Java.util包下
-
集合只能保存对象(实际是对象的引用变量),不可以保存基本数据类型的变量
-
Java 的集合类主要由两个接口派生出来:Collection 和 Map
Collection 接口下派生出:Set、List 、Queue
其中:
- Set:无序集合,元素不可重复
- List:有序集合,元素可以重复
- Queue:队列
Map实现类用于保存 具有映射关系的数据 (也就是关联数组)
Map保存的每项数据都是 key—value 对(键值对),也就是由 key 和 value 两个值组成。key 是不可重复的,key 用来标识集合里的每项数据,根据 Map 的 key 来查阅Map 中的数据
-
对于 Set、List、Map、Queue 四种集合,常用的实现类有:HashSet、TreeSet、ArrayList、ArrayQueue、HashMap、TreeMap
Collection接口和 Iterator 接口
collection中的常用方法
boolean add(Object o) //向集合中添加元素,添加成功返回true,失败返回false
boolean addAll(Collection c) //把集合c中的元素添加到指定集合,添加成功返回true
void clear() //清除集合中的元素,将集合长度变为0
int size() //返回集合里元素的个数
Object[] toArray() //把集合转换成一个数组
boolean contains(Object o) //集合里是否包含某个元素
boolean containsAll(Collection c) //集合里是否包含集合c中的所有元素
boolean isEmpty() //集合是否为空,当集合长度为0时返回true,否则返回false
boolean remove(Object o) //删除集合中的指定元素e,当集合中包含多个元素o时,该方法只删除第一个符合条件的元素
boolean removeAll(Collection c) //从集合中删除集合c里包含的所有元素(相当于该集合减去集合c,差集)
boolean retainAll(Collection c) //从集合中删除集合c里不包含的所有元素(相当于把集合变成该集合和集合c的交集)
Iterator iterator() //返回一个Iterator对象,用于遍历集合里的元素
遍历Collection集合
-
使用 Lambda 表达式遍历集合
//Set 接口继承了 Collection 接口,Collection 接口 继承了 Iterable 接口 Iterable接口中定义了 forEach(Cousumer action) 方法,该方法所需要的参数的类型是函数式接口 public class CollectionTs { public static void main(String[] args) { Dog d1 = new Dog("大黄"); Dog d2 = new Dog("小黑"); Set s = new HashSet(); s.add(d1); s.add(d2); s.forEach(Dog -> System.out.println(Dog)); } }
-
使用 Iterator 遍历集合元素(while循环 + next读取)
Iterator 接口 也是 Java 集合框架的成员,与 Collection 系列、Map 系列不一样的是:Collection、Map 系列集合主要用于盛装其他对象,而 Iterator 主要用于遍历(即迭代访问)Collection 集合中的元素,Iterator 对象也被称为 迭代器
Iterator 接口提供了如下四个方法:
boolean hasNext() //如果还有下一个元素返回true Object next() //返回集合中的下一个元素 void remove() //删除集合里上一次 next 方法返回的元素(所以必须先使用next,才能使用remove) void forEachRemaining(Consumer action) //该方法可以使用Lambda 表达式来遍历集合元素 remaining:剩余的
public class IteratorTest { public static void main(String[] args) { Set books = new HashSet(); books.add("疯狂java讲义"); books.add("百年孤独"); //获取 books集合对应的迭代器 Iterator it = books.iterator(); while (it.hasNext()){ String book = (String)it.next(); System.out.println(book);//疯狂java讲义 百年孤独 if(book.equals("百年孤独")){ it.remove(); } //对book变量重新赋值,不会改变集合元素本身 book = "重新赋值的字符串"; } System.out.println(books);//[疯狂java讲义] } }
Collection 集合里 定义了 Iterator 属性,所以Collection 及其子类 类型的对象可以直接调用它们各自的迭代器属性,Iterator 仅用于遍历集合,本身没有盛放对象的能力,所以,如果要创建Iterator 对象,则必须有一个被迭代的集合。没有集合的 Iterator 就像无本之木,没有价值
当使用 Iterator 对集合进行迭代时,Iterator 并不是把集合元素本身传给迭代变量,而是把集合元素的值传给了迭代变量,所以修改迭代变量的值对集合本身没有影响
注意: ! ! ! ! Iterator 创建一次只能使用一次遍历,下一次遍历话需要创建新的 Iterator 实例
集合对象每次调用迭代器都会得到一个全新的迭代器对象,默认的指针都在集合第一个元素之前
迭代器是位于集合元素的中间,如下图,这是越过了第一个元素,也就是说,已经读取完了第一个元素的 值。iterator.next();
-
使用 Lambda 表达式遍历 Iterator
Set books = new HashSet(); books.add("疯狂java讲义"); books.add("百年孤独"); //获取 books集合对应的迭代器 Iterator it = books.iterator(); //使用Lambda 表达式遍历集合元素 it.forEachRemaining(o -> System.out.println(o));
-
使用 forEach 循环遍历集合元素
for(Object book : books){ System.out.println(book); }
操作集合
-
使用 Predicate 过滤集合
//Java 8 起为 Collection 集合新增了一个 removeIf(Predicate filter) 方法,该方法将会批量删除符合 filter 条件的所有元素。该方法需要一个 Predicate 对象作为参数,Predicate 也是函数式接口,因此可使用 Lambda 表达式作为参数。 //示例使用 Predicate 来过滤集合。 public class ForeachTest { public static void main(String[] args) { // 创建一个集合 Collection objs = new HashSet(); objs.add(new String("中文百度搜索Java教程")); objs.add(new String("中文百度搜索C++教程")); objs.add(new String("中文百度搜索C语言教程")); objs.add(new String("中文百度搜索Python教程")); objs.add(new String("中文百度搜索Go教程")); // 使用Lambda表达式(目标类型是Predicate)过滤集合 objs.removeIf(ele -> ((String) ele).length() < 12); System.out.println(objs); } } //上面程序中第 11 行代码调用了 Collection 集合的 removeIf() 方法批量删除集合中符合条件的元素,程序传入一个 Lambda 表达式作为过滤条件。所有长度小于 12 的字符串元素都会被删除。编译、运行这段代码,可以看到如下输出: [中文百度搜索Java教程, 中文百度搜索Python教程] //使用 Predicate 可以充分简化集合的运算,假设依然有上面程序所示的 objs 集合,如果程序有如下三个统计需求: //统计集合中出现“中文百度搜索”字符串的数量。 //统计集合中出现“Java”字符串的数量。 //统计集合中出现字符串长度大于 12 的数量。 //此处只是一个假设,实际上还可能有更多的统计需求。如果采用传统的编程方式来完成这些需求,则需要执行三次循环,但采用 Predicate 只需要一个方法即可。下面代码示范了这种用法。 public class ForeachTest { public static void main(String[] args) { // 创建一个集合 Collection objs = new HashSet(); objs.add(new String("中文百度搜索Java教程")); objs.add(new String("中文百度搜索C++教程")); objs.add(new String("中文百度搜索C语言教程")); objs.add(new String("中文百度搜索Python教程")); objs.add(new String("中文百度搜索Go教程")); // 统计集合中出现“中文百度搜索”字符串的数量 System.out.println(calAll(objs, ele -> ((String) ele).contains("中文百度搜索"))); // 统计集合中出现“Java”字符串的数量 System.out.println(calAll(objs, ele -> ((String) ele).contains("Java"))); // 统计集合中出现字符串长度大于 12 的数量 System.out.println(calAll(objs, ele -> ((String) ele).length() > 12)); } public static int calAll(Collection books, Predicate p) { int total = 0; for (Object obj : books) { // 使用Predicate的test()方法判断该对象是否满足Predicate指定的条件 if (p.test(obj)) { total++; } } return total; } } //上面程序先定义了一个 calAll() 方法,它使用 Predicate 判断每个集合元素是否符合特定条件,条件将通过 Predicate 参数动态传入。从上面程序中第 11、13、15 行代码可以看到,程序传入了 3 个 Lambda 表达式,其目标类型都是 Predicate,这样 calAll() 方法就只会统计满足 Predicate 条件的图书。
-
使用 Stream 操作集合
Set 集合
- Set 集合与 Collection 基本相同,没有提供任何额外的方法,实际上 Set 就是 Collection 只是行为略许不同(Set 不允许包含重复的元素)
HashSet
【HashSet 底层是数组(HashSet 构造器直接创建了一个HashMap,而哈希表的主干是数组) 首先创建长度为16的数组,添加元素时先通过 HashCode() 方法获取其哈希值,然后通过哈希函数(又叫散列函数)来计算其在数组中存储的位置,也就是说即使哈希值不同也有可能存储的位置相同(因为它是通过函数计算出来的x不同y可能相同)(存储位置相同即产生了哈希冲突,HashMap采用了链地址法解决哈希冲突,就是数组+链表的方式,所有通过哈希函数得到同一地址的元素通过链表加在后面即可),当然如果哈希值一样那存储位置肯定一样;
如果要添加的元素通过计算得到的存储位置已经有元素了并且它们的哈希值不相同,那就通过链表来存储新的元素(把新的元素和原来位置的元素通过链表连接,jdk8中原来的元素在数组中指向新添加的元素)也就是所谓的一个"桶" 中放了多个元素;如果要添加的元素通过计算得到的存储位置已经有元素了并且它们的哈希值也相同,新要添加的元素调用其所在类的 equals() 方法和原来桶中的元素比较,如果 equals() 方法的返回值也为 true,则添加失败,如果 equals() 方法返回值为 false,则仍然是通过链表来存储新的元素,同样是一个"桶"中放了多个元素】
调用 hashCode()
--> 哈希值
--> 数组中的存储位置
--> if(位置没有元素) --> 添加成功
else(位置有元素)
-->if(哈希值相同)
--> if(equals比较相同)-->添加失败
-->else(equals比较不同)-->添加成功
else(哈希值不同和所有的比) --> 添加成功
HashSet 按照 Hash 算法存储集合中的元素,具有良好的存取和查找性能
HashSet 的特点:
- 不能保证元素的排列顺序,顺序可能与添加顺序不同,可能发生变化
- HashSet 是线程不安全的(不是同步的),如果多个线程同时访问一个 HashSet ,需要通过代码保证同步
- 集合元素可以是null
当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据该 hashCode 值决定该对象在 HashSet 中的存储位置。如果两个对象通过 equals()方法比较返回 true ,但它们的 hashCode()方法的返回值不同,HashSet 会把它们存储到不同的位置,依然可以添加成功。也就是即使两个 A 对象通过 equals() 方法返回true,但由于它们的 hashCode 值不同,HashSet 依然会把他们当成两个对象;即使两个B 对象 通过hashCode()返回 true,但由于它们的equals()返回false,HashSet 也会把它们当成两个对象
HashSet 集合判断两个元素相等的标准是两个对象通过 equals() 方法比较相等并且两个对象的 hashCode()方法的返回值也相等
所以,当把一个对象放入HashSet中时,如果需要重写该对象对应类的 equals() 方法,则也应该重写其 hashCode() 方法 ,规则是:如果两个对象通过 equals()方法比较返回true,这两个对象的 hashCode 值也应该相同
如果需要把某个类的对象保存到 HashSet 集合中,重写这个类的 equals() 和 hashCode() 方法时,应该尽量保证两个对象通过 equals() 比较返回 True 时,它们的 hashCode() 方法的返回值也相同
HashSet 中每个能存储元素的”槽位“(slot)通常被称为 “桶”(bucket),如果多个元素的hashCode值相同,但他们通过equals()方法比较返回 false ,就需要在一个桶里放多个元素,这样会导致性能下降
重写 hashCode() 方法的基本规则:
- 在程序运行过程中,同一个对象多次调用 hashCode() 方法返回的值应该相同
- 当两个对象通过 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值应该相同
- 对象中用作 equals() 方法比较标准的实例变量,都应该用于计算 hashCode 值
hash(哈希、散列)算法的功能是,他能保证快速查找被检索的对象,hash 算法的价值在于速度,当需要查询集合中的某个元素时,hash 算法可以直接根据该元素的 hashCode值计算出该元素的存储位置,从而快速定位该元素,当向 HashSet中添加元素时,它会根据该元素的 hashCode 值来计算它的存储位置,这样也可以快速定位该元素。
HashSet相对于数组的好处是:数组的索引是连续的,且数组的长度是固定的
LinkedHashSet
LinkedHashSet 集合在 HashSet 的基础上 使用链表维护元素的次序,这样,当遍历 LinkedHashSet 集合里的元素时,LinkedHashSet 将会按照元素的添加顺序来访问集合里的元素
LinkedHashSet 需要维护元素的插入顺序,因此性能略低于 HashSet ,但在迭代访问 Set 集合里的全部元素时具有很好的性能
TreeSet
TreeSet是 sortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态,
TreeSet 只能添加同一种类型的对象
与HashSet 集合相比,TreeSet 还提供了如下几个额外的方法:
Comparator comparator() //如果TreeSet 采用定制排序,返回定制排序所用的比较器comparator,如果采用自然排序,返回null
Object first() //返回集合中的第一个元素
Object last() //返回集合中的最后一个元素
Object lower(Object e) //返回集合中位于指定元素的前一个元素
Object higher(Object e) //返回集合中位于指定元素的后一个元素
SortedSet subSet(Object fromElement, Object toElement) //返回此Set集合的子集,范围从fromElement 到 toElement 区间左闭右开
SortedSet headSet(Object toElement) //返回此Set的子集,范围从开始到toElement(不包含)
SortedSet tailSet(Object fromElement) //返回此Set的子集,由大于等于fromElement的元素组成
TreeSet 是根据元素的实际值的大小来排序的,不是插入的顺序
TreeSet 采用红黑树的数据结构来存储集合元素,
TreeSet 指出两种排序方法:自然排序 和 定制排序
自然排序:
将集合元素按照升序排列 ,调用元素的compareTo(Object o) 方法来比较大小关系
Java提供了一个 Comparable 接口,其中定义了一个 compareTo(Object o)方法用于比较大小,详见 Java比较器
如果视图把一个对象添加到 TreeSet ,则该对象所在的类必须实现 Comparable 接口,否则会报ClassCastException 异常
TreeSet 只能添加同一种类型的对象,否则也会引发 ClassCastException 【大部分类在实现 compare To 方法时,都需要将被比较的对象强制转换成相同的类型,当试图把一个对象添加到 TreeSet 集合时,TreeSet 会调用该对象的 compareTo 方法与集合中的元素进行比较—这就要求集合中的元素和该元素是同一个类的实例】
TreeSet 判断两个对象相等的标准是:两个对象通过 compareTo(Object obj)方法比较是否返回 0 返回0 则相等,由此应该注意一个问题:当需要把一个对象放入 TreeSet 中,重写该对象对应类的 equals()方法时,应该保证 equals() 方法与 compareTo 方法有一致的结果,如果两个对象通过 equals() 方法返回 true 则这两个对象通过 compareTo 方法比较应该返回 0
定制排序
可以通过 Comparator 接口来实现定制排序:该接口中包含 一个 int compare(T o1, T o2) 方法,该方法用于比较 o1 和 o2 的大小,如果返回 正整数,则表明 o1 大于 o2 ; 如果返回 0 则相等,如果返回负整数,则o1 小于 o2
实现定制排序,需要在创建TreeSet的实例时,提供一个 Comparator 对象与该TreeSet 集合关联,由该Comparator 对象负责集合元素的排序逻辑,由于Comparator 接口是一个函数式接口,因此可以使用 Lambda 表达式来代替 Comparator 对象
-
//定制排序 TreeSet ts = new TreeSet((o1,o2) -> { Men m1 = (Men) o1; Men m2 = (Men) o2; return m1.age > m2.age ? -1 : m1.age < m2.age ? 1 : 0; }); ts.add(new Men(5)); ts.add(new Men(9)); ts.add(new Men(3)); System.out.println(ts);
EnumSet
EnumSet 是一个专门为枚举类设计的集合类,EnumSet 中所有元素都必须是指定枚举类型的枚举值
-
各 Set 实现类的性能分析
HashSet 和 TreeSet 怎么选择?HashSet 的性能总是比 TreeSet 好(特别是最常用的添加、查询等操作)因为TreeSet 需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的 Set 集合时,才使用 TreeSet 集合,否则都应该使用 HashSet
HashSet 还有一个子类:LinkedHashSet ,对于普通的插入、删除操作,LinkedHashSet 比 HashSet 略微慢一点,这是由于维护链表所造成的额外开销导致的,但由于有了链表,遍历 LinkedHashSet 会更快
EnumSet 是所有 Set 实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素
Set 的三个实现类 HashSet 、TreeSet 、EnumSet 都是线程不安全的。如果有多个线程同时操作一个 Set 集合,必须手动保证 该 Set 集合的 同步性【通常可以通过 Collections 工具类的 synchronizedSortedSet 方法来包住该 Set 集合 。此操作最好在创建时进行,以防止对Set集合的意外的非同步访问】
SortedSet s = Collections.synchronizedSortedSet(new TreeSet);
List 集合
list 集合代表一个元素有序、可重复的集合,List 集合默认按照元素的添加顺序设置元素的索引,
可以把 List 看成是动态数组(容量可变)initialcapacity
改进的List接口和 ListIterator 接口
-
除了 Collection 接口中的方法,List集合中增加了根据索引来操作集合元素的方法
//常用的方法:增、删、改、查、插、长度、遍历 void add(int index, Object element) //将元素element添加到指定索引处 Object remove(int index) // 删除并返回index索引处的元素 Object set(int index, Object element)//将 index 索引处的元素替换成指定元素element,返回被替换的元素 Object get(int index) // 返回指定索引处的元素 int size(); //返回集合的长度 //非常用方法 List subList(int fromIndex, int toIndex)//返回指定索引区间的子集合,左闭右开 void sort(Comparator c) //根据Comparator参数对 List 集合的元素进行排序 boolean addAll(int index, Collection c) //将集合 c中的所有元素插入指定索引处 int indexOf(Object o) //返回对象o在List集合中第一次出现的位置,不存在返回-1 int lastIndexOf(Object o) //返回对象 o 在 List 集合中最后一次出现的位置,如果不存在返回-1
-
List 还额外提供了一个 listIterator() 方法,该方法返回一个 ListIterator 对象,ListIterator 接口继承了 Iterator 接口,还提供了专门操作 List 的方法
boolean hasPrevious()//返回该迭代器关联的集合是否有上一个元素 Object previous() //返回该迭代器的上一个元素 void add(Object o)//在指定位置插入一个元素
ArrayList 和 Vector
Arraylist 和 Vector 是 List 的两个实现类
ArrayList 和 Vector 都是基于数组实现的 List 类 ,它们内部封装了一个 Object[] 类型的动态数组
ArrayList 可以使用 initialCapacity 参数来设置该数组的长度,如果向 ArrayList 添加大量元素时,可以使用 ensureCapacity(int minCapacity) 【确定容量是否够】方法一次性的增加 initialCapacity
//右移一位相当于除2 如:10 >> 1 = 5
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//右移一位相当于除2,再加上原来旧的 capacity就是扩容为原来的1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
如果开始就知道 ArrayList 集合需要保存多少个元素,则可以在创建它们的时候就指定 initialCapacity 的大小,如果不指定 initialCapacity ,则默认的Object[] 数组长度为10
jdk8中 ArrayList 在创建对象的时候并没有在底层创建一个长度为10的 Object[] 数组,而是在第一次调用 add() 方法往里面添加元素的时候创建了长度为10的数组
ArrayList 和 Vector 在用法上几乎完全相同,由于Vector 是一个古老的集合(从jdk1.0开始就有)那时候 Java 还没有提供系统的集合框架,所以Vector 里有一些重复的方法
另外,ArrayList 和 Vector 的显著区别是**:ArrayList 是线程不安全的,但 Vector 是线程安全的**,所以 Vector 的性能要比 ArrayList 低,即使这样也推荐使用 Vector 类
LinkedList
LinkedList 还实现了 Deque(双端队列) 接口,所以可以被当成双端队列来使用,因此既可以当成 栈 来使用,也可以当成队列来使用
LinkedList 和 ArrayList、ArrayDeque 的实现机制完全不同,ArrayList、ArrayDeque 是使用 数组 来存储数据,因此随机访问集合元素时具有良好的性能;而 LinkedList 内部以双端链表的形式来存储,因此随机访问集合元素的性能较差,但是在插入、删除集合元素时具有良好的性能(只需要改变指针所指的地址就可以)
ps: 对于所有内部基于数组的集合实现,如:ArrayList、ArrayDeque等,使用随机访问的性能都比使用Iterator 迭代访问的性能要好,因为随机访问会被映射成对数组元素的访问
Queue
-
队列是先进先出的容器,插入(offer)只能从队列的尾部操作,访问(poll)元素只能从队列的头部,通常队列不允许随机访问队列中的元素
//队列中常用的方法 void add(Object e);//将指定的元素插入到此队列的尾部 Object element();//获取队列头部的元素 Object peek(); //获取队列头部元素,如果此队列为空,则返回null Object poll(); //获取队列头部元素,并删除该元素,如果此队列为空,则返回null Object remove(); //删除队列头部元素并返回该元素 boolean offer(); //将指定元素插入到队列尾部。当使用有容量限制的队列时,此方法比 add 更好
PriorityQueue
- PriorityQueue (优先队列)保存元素的顺序并不是按照加入队列的顺序,而是按队列元素的大小进行了重新排序。
- 因为需要对队列元素进行排序,所以 PriorityQueue 不允许插入 null 元素,它有两种排序方式:
- 自然排序:从小到大升序排列,需要实现 Comparable 接口,规则同 TreeSet
- 定制排序:创建 PriorityQueue 队列时,传入一个 Comparator 对象,该对象负责对队列中的元素进行排序,采用定制排序时不要求队列元素实现 Comparable 接口
Deque 接口与 ArrayDeque 实现类
-
Deque 是 Queue 接口的子接口,双端队列可以同时从两端来添加、删除元素,所以 Deque 的实现类也可以当成 栈 来使用
Deque 接口中定义的一些方法
void addFirst(Object e); //将指定元素插入到该双端队列的开头 void addLast(Object e); //将指定元素插入到该双端队列的末尾 boolean offerFirst(Object e); //将指定元素插入到该双端队列的开头 boolean offerLast(Object e); //将指定元素插入到该双端队列的末尾 Object getFirst(); //获取双端队列的第一个元素 Object getLast(); //获取双端队列的最后一个元素 Object peekFirst(); //获取双端队列的第一个元素,若此双端队列为空,返回null Object peekLast(); //获取双端队列的最后一个元素,若此双端队列为空,返回null Object pollFirst(); //获取双端队列的第一个元素,并删除,若此双端队列为空,返回null Object pollLast(); //获取双端队列的最后一个元素,并删除,若此双端队列为空,返回null Object removeFirst(); //删除双端队列的第一个元素并返回 Object removeLast(); //删除双端队列的最后一个元素并返回 Iterator descendingIterator(); //返回该双端队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素 Object pop(); //(栈方法:出栈) pop出该双端队列所表示的栈的栈顶元素。相当于 removeFirst void push(Object e); //(栈方法:压栈) 将一个元素 push 进该双端队列所表示的栈的栈顶,相当于 addFirst()
ArrayDeque 底层也是采用的 Object[] 动态数组来存储集合元素
当程序中需要使用 “栈” 这种数据结构时,推荐使用 ArrayDeque 避免使用 Stack -----古老的集合,性能差
Map 集合
-
Map 用于保存具有映射关系的数据,Map又被称为关联数组。
-
Map 中保存的是 Key - value 对,如果把 Map 里所有的 key 放到一起来看,它们就组成了个 Set 集合(所有的 key 没有顺序,不可重复),实际上 Map 中有一个 keySet() 方法,用于返回 Map 里所有 key 组成的 Set 集合,把Map 里所有的 value 放到一起来看,又非常类似一个 List(元素之间可以重复,每个元素根据索引查找)只不过索引变成了 key 对象
-
实际上,Java 是先实现了 Map,然后通过包装一个所有的 value 都为空对象的 Map 来实现了 Set集合
-
Entry 是 Map 的一个内部接口,Entry 将 键值对的对应关系 封装成一个对象 ,即键值对对象
在往 Map 中添加(put)对象时,实际是添加(put)的 Entry,Entry中封装着 key-value
Entry 也可以看作是 Set 盛装的(无序,不可重复),实际也有 Set entrySet()方法
//Entry 包含的方法 Object getKey(); //获取key Object getValue(); //获取value Object setValue(V value); //设置该 Entry 对象里包含的 value,并返回新设置的value
-
Map中常用的方法
Object put(Object key, Object value); //往 Map 中存放元素,如果已经存在相同的键值对,则新的会覆盖旧的(见底层原理) void putAll(Map m); // Object remove(Object key); //根据key 删除元素,返回被删除的键值对的value boolean remove(Object key, Object value); //根据key-value删除,成功返回true 失败返回false Object replace(Object key, Object value); //将指定的旧的键值对替换成新的,和put()不同的是如果所指定的key不存在,该方法不会添加 key-value 对,而是返回null boolean replace(K key, V oldValue, V newValue); //替换,成功返回ture,失败返回false Object get(Object key); //根据key查找指定 value,不存在返回 null int size(); //返回键值对个数 void forEach(BiConsumer action); //遍历 Set entrySet(); //返回所有的 Map.Entry 对象组成的Set集合 Set keySet(); //返回所有 key 组成的Set集合 Collection values(); //返回所有 value 组成的Collection集合 boolean containsKey(Object key); //是否包含指定key boolean containsValue(Object value); //是否包含指定value void clear(); //清空所有元素,把map.size变为0 //Map的遍历 //通过keySet() 得到key的Set集合再用 Set的遍历方式,通过values() 得到value的Collection集合再遍历 //通过 entrySet() 得到 entry集合 再遍历
HashMap 和 Hashtable
-
HashMap 和 Hashtable 类似于 ArrayList 和 Vector 的关系,
-
Hashtable 是一个古老的实现类(甚至没有遵循命名规范),线程安全,性能低一点,不允许使用 null 作为 key 或 value,不推荐使用
-
HashMap 线程不安全,性能好,允许使用 null 作为 key 或 value(但由于key不能重复,所以最多只能有一个 key 为 null)
-
如果key 使用自定义类对象来充当,因为 key 对象要求不可重复,无序(set),所以要求被用作 key 对象 所在的类必须重写 hashCode() 和 equals() 方法
类似于 HashSet , HashMap 判断两个 key 对象相等的标准也是:两个key 通过 equals() 和 hashCode() 方法返回结果一致;两个value相等的标准是 通过 equals() 方法返回 true 即可
-
HashMap 的底层实现原理
HashMap map = new HashMap();
以空参构造器为例:底层创建了一个长度为 16 的 Entry[] 类型的名字为 table 的数组
map.put(key, value);
添加元素时,首先调用 key 的 hashCode() 方法得到 哈希值(并不是直接hashCode()返回的值,(h = key.hashCode()) ^ (h >>> 16)),然后通过哈希函数计算出 该哈希值在 Entry数组中的存储位置;
如果此位置没有数据,则直接添加成功;
如果此位置已有数据(一个或多个),(存储位置相同即产生了哈希冲突,HashMap采用了链地址法解决哈希冲突,就是数组+链表的方式,所有通过哈希函数得到同一地址的元素通过链表加在后面即可) 就比较要添加的 key 的哈希值和已经在集合中的数据(一个或多个,都要比较)的哈希值,如果没有相同的,则添加成功(链表的形式,旧元素指向新元素(jdk8));如果哈希值存在相同的,就继续调用该 key 的 equals() 方法比较是否相同;如果 false,添加成功,如果 true ,使用该 value 替换已经存在的 value 【所以 put() 方法如果重复就替换旧的value】
Jdk8中新特性:
-
new HashMap() :底层没有立即创建一个长度为16 的 Node数组
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; //加载因子赋值,0.75 }
-
Jdk8 中底层的数组是 Node[] 而不是 Entry[]
transient Node<K,V>[] table; static class Node<K,V> implements Map.Entry<K,V> { ...... }
-
首次调用 put() 方法时创建长 16 的 Node[]数组
-
Jdk8中底层数据结构是:数组 + 链表 + 红黑树 ;Jdk 7底层机构只有 :数组 + 链表
当数组某个位置上的元素以链表形式存在的数据个数超过 8 且当前数组的长度超过 64 时,此索引位置上的所有数据改为用红黑树存储(遍历快,方便查找,红黑树是二叉排序树左小右大)
-
-
扩容问题
-
HashMap中的一些术语
DEFAULT_INITIAL_CAPACITY //默认初始容量 16 MAXIMUM_CAPACITY //最大容量 2^30 DEFAULT_LOAD_FACTOR //默认加载因子 0.75 factor 因素,因子 TREEIFY_THRESHOLD // Bucket(桶)中链表长度的默认值 值为8 threshold 门槛,阈值 UNTREEIFY_THRESHOLD // Bucket中红黑树存储的Node小于该默认值,转化为链表 值为6 MIN_TREEIFY_CAPACITY //Bucket中链表要转化为红黑树时HashMap的最小容量 64 table://存储元素的数组,总是2的n次幂 modCount //HashMap扩容和结构改变的次数。 threshold //扩容的临界值 = 容量 * 填充因子 loadFactor //加载因子(填充因子)数组容量达到 总容量 * 加载因子 就开始扩容
-
与 HashSet 类似,如果改变 key对象的属性,则可能出现无法精确访问到 Map 中被修改过的 key,
如果在向 HashSet 中添加了一个对象后,再该变了此对象的某些属性,使得它和 HashSet中已存在的对象相同,这就有可能导致 HashSet 中存在相同的元素
@Test public void test1(){ Set set = new HashSet(10); Games g1 = new Games("三国杀"); set.add(g1); set.add(new Games("绝地求生")); set.add(new Games("穿越火线")); System.out.println(set);//[Games{name='三国杀'}, Games{name='穿越火线'}, Games{name='绝地求生'}] g1.name = "绝地求生"; //改变了g1的属性使得和其他的Games对象相同 System.out.println(set);//[Games{name='绝地求生'}, Games{name='穿越火线'}, Games{name='绝地求生'}] }
-
HashMap 底层:数组 + 链表 + 红黑树 (jdk8)
-
Properties
Properties 是 Hashtable 类的一个子类,常用来处理 配置文件,
Properties 里的 key、value 都是 String 类型
void load(InputStream inStream); //加载文件 String getProperty(String key); String getProperty(String key, String defaultValue); String setProperty(String key, String value);
LinkedHashMap
- 与 LinkedHashSet 类似,LinkedHashMap 也使用双向链表来维护 key-value 对的顺序,该链表负责维护 LinkedHashMap 的迭代顺序,使得迭代顺序与插入顺序保持一致
- LinkedHashMap 需要维护元素的插入顺序,因此性能略低,但他在迭代访问(遍历)时具有良好的性能
TreeMap
-
TreeMap 是 SortedMap 接口的实现类,
-
TreeMap 就是一个 红黑树 数据结构,每个键值对即作为红黑树的一个节点(node),TreeMap 存储键值对(节点)时,需要根据 key 来排序
-
TreeMap 有两种排序方式:自然排序、定制排序
-
自然排序(从小到大升序)
key对象所在的类需要实现 Comparable 接口,并且 所有的 key 属于同一个类
-
定制排序
创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对key排序
-
key 相等的标准,两个 key 通过 compareTo() 方法返回 0
如果使用自定义类作为 key 则要重写该类的 equals() 方法使得其结果和 compareTo() 方法的结果一致
-
TreeMap 中额外的方法
和TreeSet 类似,增加了访问第一个,最后一个,前一个,后一个元素的方法,截取子TreeMap的方法
//和TreeSet 类似,增加了访问第一个,最后一个,前一个,后一个元素的方法,截取子TreeMap的方法 Object firstKey(); //返回第一个key Map若为空,返回null Object lastKey(); //返回最后一个key,若为空返回null Object lowerKey(Object key); //返回该key前一个key,为空返回null Object higherKey(); //返回该key后一个key,为空,返回null SortedMap subMap(Object fromKey, Object toKey); //返回指定区间子map 左闭右开
WeakHashMap
- 与 HashMap 基本相同,区别在于,HashMap 的 key 保留了对实际对象的强引用,WeakHashMap 是弱引用
IdentityHashMap
- 与HashMap基本相同,区别是,它在处理两个 key 相等时的比较方式不同,:在 IdentitHashMap 中,当且仅当两个 key 严格相等(key1 == key2)时,IdentityHashMap 才认为两个 key 相等,而HashMap 是只要 equals()方法返回 true 且它们的 hashCode值相等即可
EnumMap
- 所有的 key 必须是单个枚举类的枚举值
CurrentHashMap
各 Map 实现类性能分析
-
TreeMap通常比HashMap要慢(尤其在插入、删除key-value时更慢),因为TreeMap底层采用红黑树来管理key-value对(红黑树的每一个节点就是一个key-value键值对)。
使用TreeMap有一个好处:TreeMap中的key-value总是处于有序状态,无须专门进行排序操作,当TreeMap被填充之后,就可以调用keySet(),取得由key组成的Set,然后使用toArray()方法生成key的数组,接下来使用Arrays的binarySearch()方法在已排序的数组中快速查找对象。
对于一般的应用场景,程序应该多考虑使用HashMap,因为HashMap正是为了快速查询而设计的(HashMap底层其实也是 采用数组来存储key-value对),但如果程序需要一个总是排好序的Map,则可以考虑使用TreeMap。
LinkedHashMap比HashMap慢一点,因为它需要维护链表来保持Map中key-value添加时的顺序。
IdentityHashMap性能没有特别出色之处,因为它采用与HashMap基本相似的实现,只是它使用 == 而不是equals方法来判断元素相等。
EnumMap性能最好,但它只能使用同一个枚举类的枚举值作为key。
HashMap 和 HashSet 的性能
Collections
-
操作 Set、List 和 Map 的工具类
-
常用的方法(排序、查修、修改、同步)
void reverse(List list);//反转List集合元素的顺序 void shuffle(List list);//对 List 的元素进行随机排序 void sort(List list);//自然排序,升序 void sort(List list, Comparator c);//定制排序 void swap(List list, int i, int j);//交换 i 和 j 位置的元素 Object max(Collection coll);//给定集合最大元素(自然排序) Object max(Collection coll, Comparator comp);//返回最大元素(定制排序) Object min(Collection coll);//最小元素(自然排序) Object max(Collection coll, Comparator c); int frequency(Collection c, Object o);//指定元素o在指定集合c中出现的次数(频率) void copy(List dest, List src);//将 src 中的内容 复制到 dest 中 (dest 的 size(注意是size不是capacity) 要大于等于 src 的size 否则会报角标越界异常) 可以这样创建dest : List dest = Arrays.asList(new Object[src.size]); boolean replaceAll(List list, Object oldVal, Object newVal);//新值替换旧值 //同步控制方法 sychronsizedXxx() 将指定的集合包装成线程同步的集合 Collections.sychronsizedList(); Collections.sychronsizedMap(); Collections.sychronsizedSortedMap();
泛型
-
从 jdk 5 以后 Java 引入了"参数化模型" 的概念,允许程序在创建集合时指定集合元素的类型。Java 的参数化类型被称为泛型(Generic),所谓泛型 就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量,创建对象、调用方法时被动态的指定
-
集合使用泛型示例
List<String> strList = new ArrayList<>();//jdk7以后允许使用的菱形语法 Var strList = new ArrayList<String>(); //使用var声明时,后面不可以使用菱形语法,也就是不可以省略类型 public interface Map<K, V>{ //在该接口中K、V可以作为类型使用 V put(K key, V value) ...... } //泛型的嵌套 Set<Map.Entry<String, Integer>> entry = map.entrySet();
泛型接口、类
-
泛型形参在接口和类中可以当成类型来使用
public class Apple<T>{ private T info; }
-
泛型类的继承
//1、需要为 T 形参指明数据类型,子类不再是泛型类 public class SubApple extends Apple<String>{ } //2、子类继承了父类的泛型,子类还是泛型类 public class SubApple<T> extends Apple<T>{ }
-
生成构造器时不需要使用<>因为实际不存在泛型类,它们都是使用的同一个构造器,Apple 和 Apple 使用的构造器都是 Apple
-
注意:在静态方法、静态初始化块、静态变量的参数声明中不可以使用泛型,异常也不可以使用泛型
类的泛型是在实例化的时候指定的,静态结构加载早于实例化,所以不可以使用
类型通配符 ?
-
举例:List 对象不可以赋值给 List 对象,数组和泛型不同,String[ ] 类型的数组可以自动向上转型为 Object[ ] 但是泛型不可以
-
未来表示各种泛型类型的类(List、List等等)可以使用类型通配符( ?)
//如下方法:使用类型通配符指定c的类型 public void forEach(List<?> c){ for(int i = 0; i < c.size(); i++){ System.out.print(c.get(i)); } }
但是这种带通配符的不可以向里面添加对象,因为不确定是什么类型的,(除了null之外)
-
设定类型通配符的上限、下限
//上限,它表示泛型形参只能是 Shape 子类的 List集合 List<? extends Shape> //下限,泛型形参只能是指定类的父类 List<? super Shape>
泛型方法
-
所谓泛型方法就是在声明方法时定义一个或多个泛型形参(针对返回值类型的泛型)
修饰符 <T,S> 返回值类型 方法名(形参列表){ //方法体... } public static <E> List<E> copyArrayToList(E[] arr){ }
泛型方法可以是静态的,因为泛型参数是在调用方法时确定的,并非实在实例化类时确定的
泛型与数组
-
只能声明 List[ ] 形式的数组,但不能创建 ArrayList[10] 这样的数组
// E[] list = (E[]) new Object[10]; //以下写法是错误的 E[] list = new E[10];
IO流
-
输出:从内存持久化到硬盘中
-
输入:从硬盘输入到内存中
File 类
-
File 可以 新建 、删除 、重命名 文件和目录,但是不可以访问文件内容本身(需要IO流)
路径分隔符和系统有关:
windows和DOS系统默认使用“\”来表示
UNIX和URL使用“/”来表示
为了解决这个隐患,File类提供了一个常量: public static final String separator。根据操作系统,动态的提供分隔符。
举例: File file1 = new File(“d:\atguigu\info.txt”);
File file2 = new File(“d:” + File.separator + “atguigu” + File.separator + “info.txt”);
File file3 = new File(“d:/atguigu”);
-
File类的对象常会被作为参数传递到流的构造器中,指明读取或写入的"目的地"
-
可以操作文件和目录
File 类可以使用 文件路径字符串 来创建 File 实例,可以是绝对路径和相对路径
构造器 File file = new File("hello.txt"); File file1 = new File([parent]"E:\\zaxiang\\code","javaSenior\\src\\he.txt"); File file2 = new File(file1,[child]"hi.txt"); 1、访问文件名的相关方法 String getName(); //返回 文件名 或 路径名(如果是路径,返回最后一级子路径名) String getPath(); //返回 路径名 String getAbsolutePath(); //返回 绝对路径 String getParent(); //返回 最后一级子目录 对应的父目录 (最后一级前面的所有的) boolean renameTo(File dest); //把文件重命名为指定的文件的路径 file1.renameTo(file2)//要返回true 需要file1在硬盘中是存在的且file2在硬盘中不存在, 2.获取文件的常规信息 long length(); //返回 文件内容的长度 long lastModified(); //返回 文件的最后修改时间(毫秒数) 3.目录操作相关方法 String[] list(); //列出所有 File 对象的所有子文件名和路径名,返回String数组 File[] listFiles(); //列出所有 File 对象的所有子文件和路径 返回File数组 boolean mkdir(); //make directory 创建文件目录,如果上层目录不存在,创建失败 boolean mkdirs(); //创建目录,如果上层目录也不存在,一并创建 4.文件判断相关的方法 boolean isFile(); //是不是文件 boolean isDirectory(); //是不是目录 boolean exists(); //是否存在 boolean canRead(); //是否可读 boolean canWrite(); //是否可写 boolean isHidden(); //是否被隐藏 5.文件操作相关方法 boolean creatNewFile(); boolean delete()
流的分类
-
按不同方式分类
-
输入流和输出流(按照流的流向分)
输入输出是相对于程序运行所在的内存来说的,从内存到硬盘叫输出,从硬盘到内存叫输入
Java的输入流主要由 InputStream 和 Reader 作为父类,输出流主要由 OutputStream 和 Writer 作为基类
-
字节流和字符流(按照操作的数据单元分)
字节流操作的数据单元是 8 位的字节,适合图片、视频等非文本的数据
字符流操作的数据单元是 16 位的字符,适合处理文本
字节流主要由 InputStream 和 OutputStream作为基类,字符流主要由Reader和Writer作为基类
-
节点流和处理流(按照流的角色分)
节点流直接作用到实际的数据源上(也称为文件流)
处理流用于对一个已经存在的流进行封装(也被称为包装流)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QxgoOYhy-1625638198731)(C:\Users\Think\Pictures\Saved Pictures\电脑截图\屏幕截图 2021-05-05 1095603.png)]
-
流的体系
InputStream/Reader:所有输入流的基类(抽象类),前者是字节输入流,后者是字符输入流
OutputStream/Writer:所有输出流的基类(抽象类),前者是字节输出流,后者是字符输出流
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EKmOGTrb-1625638198733)(C:\Users\Think\AppData\Roaming\Typora\typora-user-images\image-20210531203453628.png)]
节点流(文件流)掌握)
-
输入输出流操作的四步
//1、创建文件 File file = new File("hello.txt"); //2、创建流 FileReader reader = new FileReader(file); //3、操作数据 char[] cbuf = new char[5]; int len; while ((len = reader.read(cbuf)) > 0) { for (int i = 0; i < len; i++) { System.out.print(cbuf[i]); } } //4、关闭流 if (reader != null) { reader.close(); }
-
输入流的方法(InputStream 和 Reader)
Reader 中的三个方法 int read(); //从输入流中读取单个字符,返回所读取的字节数据(可以直接强转成char类型) int read(char[] cbuf); //从输入流中一次读取cbuf.length个字符的数据,存到cbuf中返回实际读取的字符个数 int read(char[] cbuf, int off, int len); //从输入流中读取len个字符的数据,存储到cbuf中,放入cbuf数组时从数组的off位置开始放入,返回实际读取的字符个数 注意:read返回的是读入的字符的个数 InputStream 中的三个方法 int read(); //从输入流中读取单个字节,返回读取的字节数据 int read(byte[] b); //从输入流中读取 b.length 个字节的数据,返回实际读取的字节个数 int read(byte[] b, int off, int len); 从输入流中读取len个字节的数据,存储到 b 中,放入b数组时从数组的off位置开始放入,返回实际读取的字节个数
-
章节难点:cbuf 数组中的数据不会自动清理 @Test public void fileReaderTest1() { FileReader reader = null; try { //1、实例化file对象指定要操作的文件 File file = new File("E:\\zaxiang\\code\\javaSenior\\hello.txt");//相对于当 前module //2、提供具体的流 reader = new FileReader(file); //3、数据的读入 read(char[] cbuf): char[] cbuf = new char[5]; int len; while ((len = reader.read(cbuf)) != -1) { System.out.print(len); for (int i = 0; i < cbuf.length; i++) { System.out.print(cbuf[i]); } //注意此时的输出结果:5 hello 4 wordo 由于cbuf这个缓存数组会重复使用且不会清 除上一次读取的数据,所以使用i < cbuf.length 这个条件会把上次残留的数据读取进来 for (int i = 0; i < len; i++) { System.out.print(cbuf[i]); } } } catch (IOException e) { e.printStackTrace(); }finally { //4、手动关闭流(jvm不会自动回收) try { if (reader != null) { reader.close(); } } catch (IOException e) { e.printStackTrace(); } } }
-
OutputStream 和 Writer
writer中的方法 void write(int c);// void write(String str); void write(String str, int off, int len) void write(char[] cbuf); void write(char[] cbuf, int off, int len); OutputStream中的方法 void write(int c); void write(byte[] bbuf); void write(byte[] bbuf); 复制文件的操作 /** * InputStream和OutputStream 实现字节文件复制 */ @Test public void InOutStreamTest(){ FileInputStream inputStream = null; FileOutputStream outputStream = null; try { File file = new File("bmw.jpg"); File file1 = new File("copybmw.jpg"); inputStream = new FileInputStream(file); outputStream = new FileOutputStream(file1); //复制的过程 byte[] buf = new byte[5]; int len; while ((len = inputStream.read(buf)) > 0) { for (int i = 0; i < len; i++) { outputStream.write(buf[i]); } } } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
缓冲流(掌握)
-
处理流 之 缓冲流
-
BufferedInputStream 和 BufferedOutputStream
BufferedReader 和 BufferedWritervoid flush();//刷新缓存区(清空) /** * 实现非文本文件的复制 */ @Test public void bufferedStreamTest(){ BufferedInputStream bis = null; BufferedOutputStream bos = null; try { //创建文件 File srcFile = new File("bmw.jpg"); File destFile = new File("copy.jpg"); //创建流 //1、创建节点流 FileInputStream fis = new FileInputStream(srcFile); FileOutputStream fos = new FileOutputStream(destFile); //2、创建缓冲流(缓冲流是处理流,需要作用到节点流之上) bis = new BufferedInputStream(fis); bos = new BufferedOutputStream(fos); //操作数据 byte[] buffer = new byte[1024]; int len; while ((len = bis.read(buffer)) > 0) { bos.write(buffer,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { //资源关闭:要求:先关闭外层的流,再关闭内层的流 //在关闭外层流的时候内层流的关闭也会自动关闭 if(bos != null){ try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } if(bis != null){ try { bis.close(); } catch (IOException e) { e.printStackTrace(); } } } }
转换流(掌握)
-
转换流:
-
InputStreamReader: 将**字节输入流**转换成**字符的输入流**
-
OutoutStreamWriter:将**字符的输出流**转换成**字节的输出流**
/** * 综合使用转换流(可以改变文件的编码方式) */ @Test public void test() throws IOException { InputStreamReader reader = new InputStreamReader(new FileInputStream("hello.txt")); OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("hello_GBK.txt")); char[] cbuf = new char[5]; int len; while ((len = reader.read(cbuf)) > 0){ writer.write(cbuf,0,len); } writer.close(); reader.close(); }
-
对象流(序列化)
-
ObjectInputStream 和 OutputStream
-
用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可 以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
-
对象的序列化
对象序列化的目的是把对象持久化到硬盘中,序列化机制允许将 Java 对象装化为字节序列
对象的序列化(Serializable)指:将一个 Java 对象写入到 IO 流中,反序列化则指从 IO 流中恢复该 Java 对象
如果想让某个类的对象支持序列化机制,则必须让它的类是可序列化的,该类必须实现如下两个接口之一:
Serializable 或 Externalizable External(外部的)
除了当前类需要实现 Serializable 接口,还想必须保证其内部的所有属性是可序列化的(默认情况下String和基本数据类型是可序列化的),如果有其他自定义类的类型则其所在的类也需要实现 Serializable 接口
递归序列化:当对某个对象序列化时,系统会自动把该对象的所有实例变量依次进行序列化,如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化;如果被引用的对象的实例变量也引用了其他对象,则被引用的对象也会被序列化
-
所有保存到磁盘中的对象都有一个序列号:static final long serialVersionUID【serialVersionUID用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象 进行版本控制,有关各版本反序列化时是否兼容。 如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自 动生成的。若类的实例变量做了修改,serialVersionUID 可能发生变化。故建议, 显式声明。】
当程序试图序列化一个对象时,会检查该对象是否已经被序列化过,如果已经序列化过就不会再次序列化
-
如果使用序列化机制向文件中写入了多个对象,使用反序列化机制恢复对象时必须按照实际写入的顺序读取
-
-
自定义序列化
- 可以使用 transient 关键字来修饰实例变量(static修饰的变量本来就不可以被序列化),序列化的时候就不会序列化该变量
- Java 提供了一种自定义序列化机制
-
/** * 序列化:serializable 将内存中的对象保存到磁盘中 * OutputStream */ @Test public void test1(){ ObjectOutputStream oos = null; try { oos = new ObjectOutputStream( new FileOutputStream("Object.dat")); oos.writeObject(new String("中国万岁!")); oos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if(oos != null){ try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } } } /** * 反序列化 */ @Test public void test2() throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Object.dat")); String str = (String) ois.readObject();; System.out.println(str); ois.close(); }
其他流(了解)
标准的输入输出流
- System.in :标准的输入流,默认从键盘输入
- System.out : 标准的输出流,默认从控制台输出
打印流
- PrintStream
- PrintWriter
- 提供了一系列重载的print()和println()方法,用于多种数据类型的输出 PrintStream和PrintWriter的输出不会抛出IOException异常 PrintStream和PrintWriter有自动flush功能 PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。 在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。 System.out返回的是PrintStream的实例
数据流
- 用于读取和写出基本数据类型的变量或字符串
RandomAccessFile(自由访问文件)
-
它提供了众多方法访问文件内容,既支持输入流的操作也支持输出流的操作
-
RandomAccessFile 支持"随机访问"的方式,程序可以直接跳转到文件的任意地方来读写数据
"随机访问"是由 Random Access 翻译过来的,它是指可以自由访问文件的任意位置,Random不止有随机的意思还有任意的意思,类似的RAM(Random Access Memory 自由访问存储点的存储器 ,内存)
-
RandomAccessFile 直接继承于Object类,并实现了 DataInput 和 DataOutput 接口
所以既支持输入流的操作也支持输出流的操作
创建RandomAccessFile实例时,需要指定一个 mode 参数,该参数指定 RandomAccessFile 的访问模式:
- " r " :以只读的方式打开指定文件
- " rw " :以读写的方式打开指定文件
- " rws ":以读写的方式打开指定文件,还要求对文件内容和元数据的每个更新都同步写入到底层存储设备
- " rwd ":以读写的方式打开指定文件,还要求对文件内容的每个更新都同步写入到底层存储设备
-
如果RandomAccessFile作为一个输出流,写出的内容会覆盖文件中的原有内容(从文件指针的位置开始覆盖,能覆盖多少算多少,默认从0位置开始)
如果需要向指定位置插入内容,需要先把插入点后面的内容读入缓冲区,等把要插入的数据读入文件后,再将缓冲区的内容追加到文件后面
-
RandomAccessFile 对象包含了一个记录指针,用以标识当前读写的位置,一开始位于文件头,随着读写而移动;还可以自由移动该记录指针,既可以向前移动,也可以向后移动
long getFilePointer(); //返回文件指针的位置 void seek(long pos); //将文件指针定位到指定的position位置
NIO
网络编程
- 实现网络编程的两个要素
- 通信双方的地址:
- IP:网络地址(Internet Protocol)
- 用来唯一的标识 Internet 上的通信实体(可以是主机、打印机、路由器的某一个端口)
- 在Java中 使用 InetAddress 类来代表 IP
- IP 分为 IPV4 和 IPV6;万维网和局域网
- 域名:如:www.baidu.com ,通过域名的方式找对应的IP地址更容易(先把域名解析成IP)
- 本地回路地址:127.0.0.1 对应着 localhost
- 端口号:应用程序的地址
- 不同的进程有不同的端口号,被规定为一个16位的整数 0 ~ 65535
- 分类:
- 公认端口: 0 ~ 1023 被预先定义的通信服务占用(如:HTTP占用80)
- 注册端口:1024 ~ 49151( 默认 Tomcat 8080;MySql 3306;Oracle 1521 )
- 动态/私有端口:49152 ~ 65535
- 端口号和 IP 地址组合得到一个网络套接字:Socket
- IP:网络地址(Internet Protocol)
- 网络通信协议:
- TCP(Transaction )
- 使用前需要先建立 TCP 连接
- 传输前,进行三次握手,是可靠的
- 传输完毕需要释放连接,效率低
- UDP
- 将数据、源、目的 封装成数据包(报),不需要建立连接
- 每个数据包(报)的大小限制在 64K 以内
- 发送时无需确认对方是否准备好,接收方受到也不确认
- 可以广播发送
- 发送结束无需释放资源,开销小,效率高
- TCP(Transaction )
- 通信双方的地址:
InetAddress 类
InetAdress 类没有提供构造器,而是提供了如下两个静态方法来获取 InetAdress 的实例
创建InetAdress实例
getByName(String host); //根据ip地址(或域名)获取对应的InetAdress实例
getByAdress(byte[] addr);//根据原始IP地址获取InetAdress实例
getLocalHost();//当前主机IP地址对应的的InetAddress实例
常用方法:
String getHostAddress();//获取IP地址
String getHostName();//获取域名
TCP 网络编程
UDP 网络编程
URL 网络编程
-
URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址
-
URL 的基本结构由 5 部分组成
<传输协议> : // <主机名> : <端口号> / <文件名> # 片段名 ? 参数列表
Http 😕/localhost:8080/examples/index.jsp#a?username=xiaoming&password=123
反射
类的加载
-
当调用java命令运行一个 Java 程序时,会启动一个 JVM 进程,不管该程序启动了多少个线程,它们都处于 JVM 进程中,同一个程序的所有变量、线程都使用该 JVM 进程的内存区。当 Java 程序运行结束时,JVM 进程结束,该进程在内存中的状态将会丢失
-
类加载是指将类的 .class 文件读入内存,并为之创建一个 java.lang.Class 对象(加载到内存中的类就称为运行时类)
注意:系统中的所有类实际上都是 java.lang.Class 的实例
-
类的加载由类加载器完成,类加载器通常由 JVM 提供,也可以通过继承 ClassLoader 类来创建自己的类加载器
通过使用不同的类加载器,可以从不同来源加载类
- 从本地文件系统加载 .class 文件,
- 从 JAR 包加载 .class 文件 ,如:数据库驱动类就放到 JAR 文件中,JVM 可以从 JAR 文件中直接加载该class 文件
- 通过网络加载 class 文件
- 把一个 Java 源文件动态编译,并执行加载
-
类加载器通产无需等到首次使用该类时才加载该类,Java虚拟机规范允许系统预先加载某些类
通过反射查看类信息
获得Class对象
-
由于每个类被加载之后都会生成一个对应的 java.lang.Class 对象,通过该 Class 对象就可以访问 JVM中的该类
获得 java.lang.Class 对象的方式
1、调用某个类的 class 属性 如:Dog.class 推荐使用这种方式,代码安全性能好 2、使用 Class 类的静态方法 static forName(Sring clazzName);//需要传入字符串参数,值是某个类的全限定类名(必须包含包名) 3、调用某个对象的 getClass() 方法,该方法是 Object 类中的一个方法
通过 Class 类的实例方法获取信息
-
获取构造器
Constructor getConstructor(Class...parameterTypes);//返回次Class对象对应类的、带指定形参列表的public构造器 Constructor getConstructors(); //返回对应类的所有public构造器 Constructor getDeclaredConstructor(Class...parameterTypes);//.......与构造器访问权限无关 Constructor getDeclaredConstructors(); //.......与构造器的权限无关
-
获取方法
Method getMethod(String name, Class...parameterTypes);//返回对应类带指定那个形参列表的public方法
-
获取成员变量
Field getField(String name);//返回对应类的指定名称的public成员变量
使用反射生成并操作对象
-
创建对象
先获取 Constructor 对象,再调用 Constructor 对象的 newInstance() 方法创建该Class对象对应类的实例
Class clazz = Person.class; Constructor constructor = clazz.getConstructor(String.class, int.class); Object p = (Person)constructor.newInstance("Tom", 12); System.out.println(p);
- 动态/私有端口:49152 ~ 65535
- 端口号和 IP 地址组合得到一个网络套接字:Socket
- 网络通信协议:
- TCP(Transaction )
- 使用前需要先建立 TCP 连接
- 传输前,进行三次握手,是可靠的
- 传输完毕需要释放连接,效率低
- UDP
- 将数据、源、目的 封装成数据包(报),不需要建立连接
- 每个数据包(报)的大小限制在 64K 以内
- 发送时无需确认对方是否准备好,接收方受到也不确认
- 可以广播发送
- 发送结束无需释放资源,开销小,效率高
- TCP(Transaction )
InetAddress 类
InetAdress 类没有提供构造器,而是提供了如下两个静态方法来获取 InetAdress 的实例
创建InetAdress实例
getByName(String host); //根据ip地址(或域名)获取对应的InetAdress实例
getByAdress(byte[] addr);//根据原始IP地址获取InetAdress实例
getLocalHost();//当前主机IP地址对应的的InetAddress实例
常用方法:
String getHostAddress();//获取IP地址
String getHostName();//获取域名
TCP 网络编程
UDP 网络编程
URL 网络编程
-
URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址
-
URL 的基本结构由 5 部分组成
<传输协议> : // <主机名> : <端口号> / <文件名> # 片段名 ? 参数列表
Http 😕/localhost:8080/examples/index.jsp#a?username=xiaoming&password=123
反射
类的加载
-
当调用java命令运行一个 Java 程序时,会启动一个 JVM 进程,不管该程序启动了多少个线程,它们都处于 JVM 进程中,同一个程序的所有变量、线程都使用该 JVM 进程的内存区。当 Java 程序运行结束时,JVM 进程结束,该进程在内存中的状态将会丢失
-
类加载是指将类的 .class 文件读入内存,并为之创建一个 java.lang.Class 对象(加载到内存中的类就称为运行时类)
注意:系统中的所有类实际上都是 java.lang.Class 的实例
-
类的加载由类加载器完成,类加载器通常由 JVM 提供,也可以通过继承 ClassLoader 类来创建自己的类加载器
通过使用不同的类加载器,可以从不同来源加载类
- 从本地文件系统加载 .class 文件,
- 从 JAR 包加载 .class 文件 ,如:数据库驱动类就放到 JAR 文件中,JVM 可以从 JAR 文件中直接加载该class 文件
- 通过网络加载 class 文件
- 把一个 Java 源文件动态编译,并执行加载
-
类加载器通产无需等到首次使用该类时才加载该类,Java虚拟机规范允许系统预先加载某些类
通过反射查看类信息
获得Class对象
-
由于每个类被加载之后都会生成一个对应的 java.lang.Class 对象,通过该 Class 对象就可以访问 JVM中的该类
获得 java.lang.Class 对象的方式
1、调用某个类的 class 属性 如:Dog.class 推荐使用这种方式,代码安全性能好 2、使用 Class 类的静态方法 static forName(Sring clazzName);//需要传入字符串参数,值是某个类的全限定类名(必须包含包名) 3、调用某个对象的 getClass() 方法,该方法是 Object 类中的一个方法
通过 Class 类的实例方法获取信息
-
获取构造器
Constructor getConstructor(Class...parameterTypes);//返回次Class对象对应类的、带指定形参列表的public构造器 Constructor getConstructors(); //返回对应类的所有public构造器 Constructor getDeclaredConstructor(Class...parameterTypes);//.......与构造器访问权限无关 Constructor getDeclaredConstructors(); //.......与构造器的权限无关
-
获取方法
Method getMethod(String name, Class...parameterTypes);//返回对应类带指定那个形参列表的public方法
-
获取成员变量
Field getField(String name);//返回对应类的指定名称的public成员变量
使用反射生成并操作对象
-
创建对象
先获取 Constructor 对象,再调用 Constructor 对象的 newInstance() 方法创建该Class对象对应类的实例
Class clazz = Person.class; Constructor constructor = clazz.getConstructor(String.class, int.class); Object p = (Person)constructor.newInstance("Tom", 12); System.out.println(p);