Java知识回顾总结(3)

1. 多线程基础

1.1 多线程实现方式

多线程的形式上实现方式主要有两种:一种是继承 Thread 类,一种是实现 Runnable 接口。
本质上实现方式都是来实现线程任务,然后启动线程执行线程任务(这里的线程任务实际上就是run() 方法)。

继承Thread类

继承 Thread 类是最简单的一种实现线程的方式,通过JDK提供的 Thread 类,重写 Thread 类的
run() 方法即可,那么当线程启动的时候,就会执行 run() 方法体的内容。

import java.util.concurrent.TimeUnit;

public class CreateThreadDemo extends Thread {

    public CreateThreadDemo(String name){
        this.setName(name);
    }

    @Override
    public void run() {
        while(true){
            printThreadInfo();
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }

    public static void printThreadInfo(){
        System.out.println("当前运行的线程名为:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) throws Exception{
        new CreateThreadDemo("Thread1").start();

        while (true){
            printThreadInfo();
            // 有sleep就要注意 throws Exception
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

output:

当前运行的线程名为:main
当前运行的线程名为:Thread1
当前运行的线程名为:main
当前运行的线程名为:Thread1
当前运行的线程名为:main
当前运行的线程名为:Thread1
当前运行的线程名为:main
当前运行的线程名为:Thread1
当前运行的线程名为:main
当前运行的线程名为:Thread1
1. 创建多个线程

上面例子中可以看出,除了创建的一个线程 Thread1 以外,还有一个主线程 main 也在执行。c除了这两个线程以外,其实还有例如垃圾回收线程也在执行。
创建多个线程就是

new CreateThreadDemo("Thread1").start();
new CreateThreadDemo("Thread2").start();
2. 指定线程名称

在上述代码中,通过调用父类setName方法给线程名字赋值。如果不指定线程名名字,系统会默认指定线程名,命名规则是“Thread-N"的形式。

        new CreateThreadDemo().start();
        new CreateThreadDemo().start();

output:

当前运行的线程名为:main
当前运行的线程名为:Thread-1
当前运行的线程名为:Thread-0
当前运行的线程名为:main
当前运行的线程名为:Thread-0
当前运行的线程名为:Thread-1

实现Runnable接口

实现 Runnable 接口也是一种常见的创建线程的方式,使用接口的方式可以让我们的程序降低耦合度。 Runnable 接口中仅仅定义了一个方法,就是 run()Runnable 接口代码:

package java.lang;

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runnable 是一个@FunctionalInterface函数式接口,也就意味了可以利用JDK8提供的lambda的方式来创建线程任务,后续的代码中会有具体的使用。
使用 Runnable 实现上面的例子步骤如下:

  • 定义一个类,来实现 Runnable 接口,称作线程任务类。
  • 重写 run() 方法,并实现方法体,方法体中的代码就是线程所执行的代码。
  • 定义一个可以运行的类,并在 main() 方法中创建线程任务类。
  • 创建Thread类(new Thread(...).start() ),并启动线程
    创建线程任务
import java.util.concurrent.TimeUnit;

public class ThreadByRunnableDemo implements Runnable{

    @Override
    public void run(){
        while(true){
            printThreadInfo();
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch (Exception e){
                // 不太懂
                throw new RuntimeException(e);
            }
        }
    }

    public static void printThreadInfo(){
        System.out.println("当前运行的线程名为:" + Thread.currentThread().getName());
    }
}

创建可运行类

import java.util.concurrent.TimeUnit;

public class ThreadByRunnableDemo2 {
    public static void main(String[] args) throws Exception {
        ThreadByRunnableDemo task = new ThreadByRunnableDemo();

        new Thread(task).start();

        while (true){
            printThreadInfo();
            TimeUnit.SECONDS.sleep(1);
        }

    }
    public static void printThreadInfo(){
        System.out.println("当前运行的线程名为:" + Thread.currentThread().getName());
    }
}

使用内部类的方式

这并不是一种新的方式,只是另一种写法。比如有时候线程就想执行一次,那么就可以把上面两种用匿名内部类的方式来实现。

import java.util.concurrent.TimeUnit;

public class ThreadByAnonymousDemo {
    public static void main(String[] args){
        new Thread(){
            @Override
            public void run() {
                while(true){
                    try {
                        printThreadInfo();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }

    private static void printThreadInfo(){
        System.out.println("当前运行的线程名为:" + Thread.currentThread().getName());
        try {
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

基于线程池的方式

线程和数据库连接这些资源是非常宝贵的资源。每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么就需要线程池来实现缓存的策略。

相比于new Thread ,Java提供的线程池的好处在于:

  • 重用存在的线程,减少对象创建、消亡的开销。
  • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多的资源竞争和堵塞。
  • 提供定时执行、定期执行、单线程、并发数控制等功能。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadByThreadPool {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(4);

        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
            }
        });

        threadPool.shutdown();
    }
}

1.2 熟悉实现多线程的常用方法

start() :启动一个线程。使相应线程进入排队等待的状态,一旦轮到它使用CPU的资源的时候。它就可以脱离它的主线程独立开始。

run() :Thread类和Runnable接口中的run() 方法作用相同,都是系统自动调用。

sleep()wait()
sleep() :是Thread类中的方法,会使当前线程暂停执行,让出CPU的使用权限。但,监控状态依然存在,即如果当前线程进入了同步锁的话,sleep() 方法并不会释放锁,即使当前线程让出了CPU的使用权限,但其它被同步锁挡在外面的线程也无法获得执行。

wait() :是Object类的方法,wait() 方法指的是一个已经进入同步锁的线程内,让自己暂时让出同步锁,以便其它正在等待此同步锁的线程能够获得机会执行。只有其它方法调用了 notify() 或者 notifyAll() (需要注意的是调用 notify() 或者 notifyAll() 方法并不释放锁,只是告诉调用 wait() 方法的其它 线程可以参与锁的竞争了..)方法后,才能够唤醒相关的线程。此外注意 wait() 方法必须在同步关键字修饰的方法中才能调用。

notify()notifyAll() :释放因为调用 wait() 方法而正在等待中的进程。它们的唯一区别在于 notify() 唤醒某个正在等待的线程。而 notifyAll() 会唤醒所有正在等待的线程。需要需要注意的是 notify()notifyAll() 并不会释放对应的同步锁。

public class Test{
    public static void main(String[] args){
        //启动线程一
       new Thread(new Thread1()).start();
           try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
           new Thread(new Thread2()).start();
    }
}
 
class Thread1 implements Runnable{ 
    @Override
    public void run() {
    //由于这里的Thread1和Thread2的两个run方法调用的是同一个对象监视器,所以这里面就不能使用this监视器了
    //因为Thread1的this和Thread2的this指的不是同一个对象。为此我们使用Test这个类的字节码作为相应的监视器
    synchronized(Test.class){
        System.out.println("enter thread1...");
        System.out.println("thread1 is waiting...");
        try {
            //释放同步锁有两种方式一种是程序自然的离开synchronized的监视范围,另外一种方式在synchronized管辖的返回内调用了
            //wait方法,这里就使用wait方法来释放同步锁,
            Test.class.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("thread1 is going on ...");
        System.out.println("thread1 is being over ...");
    }
    }
}
 
class Thread2 implements Runnable{ 
    @Override
    public void run() {
        synchronized(Test.class){
            System.out.println("enter thread2 ...");
            System.out.println("thread2 notify other thread can release wait sattus ...");
            //由于notify不会释放同步锁,即使thread2的下面调用了sleep方法,thread1的run方法仍然无法获得执行,原因是thread2
            //没有释放同步锁Test
            Test.class.notify();
            System.out.println("thread2 is sleeping ten millisecond ...");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread2 is going on ...");
            System.out.println("thread2 is being over ...");
        }
         
    }
}

output:

enter thread1...
thread1 is waiting...
enter thread2 ...
thread2 notify other thread can release wait sattus ...
thread2 is sleeping ten millisecond ...
thread2 is going on ...
thread2 is being over ...
thread1 is going on ...
thread1 is being over ...

isAlive() :检查线程是否处于执行状态。在当前线程执行完run方法之前调用此方法会返回true。在run方法执行完进入死亡状态后调用此方法会返回false。

currentThread : Thread类中的方法,返回当前正在使用cpu的那个线程。

interrupted() : 吵醒因为调用sleep方法而进入休眠状态的方法,同时会抛出InterrruptedException。

1.3 多线程基础体系知识清单

多线程基础体系知识清单

2. I/O 数据流

Java IO 中常用的类:

IO 中常用的类

在整个Java.io包中,最重要的是5个类和1个接口。5个类指的是File , OutputStream , InputStream , Writer , Reader 。一个接口指的是Serializable 。掌握了这些IO的核心操作,那么对于Java IO体系也就有了一个初步的认识了。

Java I/O主要包括如下几个层次,包含了三个部分:
1. 流式部分——IO的主体部分;
2. 非流式部分——主要包含一些辅助流式部分的类,如:File 类、RandomAccessFile 类和FileDescriptor 等类。
3. 其他类—— 文件读取部分的与安全相关的类,如:SerializablePermission 类,以及与本地操作系统相关的文件系统的类,如 FileSystem 类和Win32FileSystem 类和WinNTFileSystem 类。
主要的类如下:

  1. File (文件特征与管理):用于文件或目录的描述信息,例如生成新目录,修改文件,删除文件,判断文件所在路径等。
  2. InputStream (二进制格式操作):抽象类。基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。
  3. OutputStream (二进制格式操作):抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。
  4. Reader (文件格式操作):抽象类。基于字符的输入操作。
  5. Writer (文件格式操作):抽象类,基于字符的输出操作。
  6. RandomAccessFile (随机文件操作):一个独立的类,直接继承至Object ,它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。

Java中IO流的体系结构:

流类的类结构图:

流的概念和作用
流代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。
流的本质:数据传输。根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
流的作用:为数据源和目的地建立一个输送通道。
Java中将输入输出抽象称为流,就好像水管,将两个容器连接起来。流式一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流。

Java IO所采用的模型
Java的IO模型设计非常优秀,它使用的Decorator(装饰者)模式,按功能划分Stream。这些Stream可以被动态装配,以获得所需功能。 例如,如果需要一个具有缓冲的文件输入流,则应当组合使用FileInputStream和BufferedInputStream。

IO流的分类

  • 根据处理数据类型的不同,分为:字符流和字节流。
  • 根据数据流向不同,分为:输入流和输出流。
  • 按数据来源(去向)分类:
  1. File(文件):FileInputStream,FileOutputStream,FileReader,FileWriter
  2. byte[]:ByteArrayInputStream,ByteArrayOutputStream
  3. Char[]:CharArrayReader,CharArrayWriter
  4. String:StringBufferInputStream,StringReader,StrinWriter
  5. 网络数据流:InputStream,OutputStream,Reader,Writer

字符流和字节流
流序列中的数据既可以使未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此,Java中的流分为两种:
1)字节流:数据流中最小的数据单元是字节。
2)字符流:数据流中最小的数据单元是字符,Java中的字符是Unicode编码,一个字符占用两个字节。
字符流的由来:Java中字符是采用Unicode标准,一个字符是16位,即一个字符使用两个字节。为此,Java中引入了处理字符的流。因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。

输入流和输出流
根据数据的输入、输出方向的不同,将流分为输入流和输出流。

  1. 输入流:程序从输入流读取数据源。数据源包括外界(键盘、文件、网络...),即是将数据源读入到程序的通信通道。
  2. 输出流:程序向输出流写入数据。将程序中的数据输出到外界(显示器、打印机、文件、网络...)的通信通道。

采用数据流的目的就是使得输出输入独立于设备。

2.1 熟悉Java常用数据流接口及它们之间的关系

1. 输入字节流InputStream

  • InputStream是所有的输入字节流的父类,它是一个抽象类。
  • ByteArrayInputStream、SteamBufferInputStream、FileInputStream是三种基本的介质流,它们分别从Byte数组、StringBuffer、和本地文件中读取数据。
  • PipedInputStream是从与其他线程共用的管道中读取数据。
  • ObjectInputStream和所有FilterInputStream的子类都是装饰流。

2. 输出字节流OutputStream


IO中输出字节流的继承图可见上图,可以看出:

  • OutputStream是所有的输出字节流的父类,它是一个抽象类。
  • ByteArrayOutputStream、FileOutputStream是两种基本的介质流,它们分别向Byte数组和本地文件中写入数据。PipedOutputStream是向与其它线程共用的管道中写入数据。
  • ObjectOutputStream和所有FilterOutputStream的子类都是装饰流。

OutputStream中的三个基本的写方法

  • abstract void write(int b) :往输出流中写入一个字节
  • void write(byte[] b) :往输出流中写入数组b中的所有字节
  • void write(byte[] b, int off, int len) :往输出流中写入数组b中从偏移量off开始的len个字节的数据
  • void flush() :刷新输出流,强制缓冲区中的输出字节被写出。
  • void close() :关闭输出流,释放和这个流相关的系统资源。

3. 字符输入流Reader


在上面的继承关系图可以看出:

  • Reader是所有的输入字符流的父类,它是一个抽象类。
  • CharArrayReader、StringReader是两种基本的介质流,它们分别将Char数组、String中读取数据。PipedReader是从与其它线程共用的管道中读取数据。
  • BufferedReader 很明显就是一个装饰器,它和其子类负责装饰其它Reader对象。
  • FilterReader是所有自定义具体装饰流的父类,其子类PushbackReader对Reader对象进行装饰,会增加一个行号。
  • InputStreamReader是一个连接字节流和字符流的桥梁,它将字节流转变为字符流。
    FileReader可以说是一个达到此功能、常用的工具类,在其源代码中明显使用了将FileInputStream转变为Reader的方法。我们可以从这个类中得到一定的技巧。Reader中各个类的用途和使用方法基本和InputStream中的类使用一致。后面会有Reader与InputStream的对应关系。
    主要方法:
    (1)public int read() throws IOException;
    读取一个字符,返回值为读取的字符
    (2)public int read(char[] cbuf) throws IOException;
    读取一系列字符到数组cbuf[] 中,返回值为实际读取的字符的数量
    (3)public abstract int read(char cbuf[], int off, int len) throws IOException;
    读取len 个字符,从数组 cbuf[] 的下标 off 处开始存放,返回值为实际读取的字符数量,该方法必须由子类实现。

4. 字符输出流Writer


在上面的关系图中可以看出:

  1. Writer是所有的输出字符流的父类,它是一个抽象类。
  2. CharArrayWriter、StringWriter是两种基本的介质流,它们分别向Char数组、String中写入数据。PipedWriter是向与其它线程共用的管道中写入数据。
  3. BufferedWriter是一个装饰器为Writer提供缓冲功能。
  4. PrintWriter和PrintStream极其类似,功能和使用也非常相似。
  5. OutputStreamWriter是OutputStream到Writer转换的桥梁,它的子类FileWriter其实就是一个实现此功能的具体类。功能和使用和OutputStream极其类似。

2.2 熟悉文件读写流

image

1. 读文件

如果是二进制文件,使用FileInputStream读取;如果是文本文件,使用FileReader读取;
这两个类允许我们从文件开始至文件结尾一个字节或字符的读取文件,或者将读取的文件写入字节数组或字符数组。如果我们想随机的读取文件内容,可以使用RandomAccessFile
基于字节流读取文件步骤:

  1. 找到指定文件
  2. 根据文件创建文件输入流
  3. 创建字节数组
  4. 读取内容,放到字节数组里面
  5. 关闭输入流
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class ReadFromFile{
    public static void main(String[] args) {
        // 构建指定文件
        File file = new File("D:\\Users\\80352170\\IdeaProjects\\CatchUp\\src\\hello_world.txt");
        InputStream input = null;
        try{
            // 根据文件创建文件的输入流
            input = new FileInputStream(file);
            // 创建字节数组
            byte[] data = new byte[1024];
            // 读取内容,放到字节数组里面
            input.read(data);
            System.out.println("文件内容:" + new String(data) );
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try{
                // 关闭输入流
                input.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

基于字符流读取文件步骤
以上示例是基于字节流读取文件,而基于字符流读取文件其实与基于字节流大同小异。

  1. 找到指定文件File file = new File("path");
  2. 根据文件创建文件输入流
Reader reader = new FileReader(file);
  1. 创建字符数组 char[] data = new char[1024];
  2. 读取内容,放到字符数组里面 reader.read(data);
  3. 关闭输入流 reader.close();

2. 写文件

基于 字节流/字符流 写入文件步骤

  1. 找到指定的文件
    File file = new File("path");
  2. 根据文件创建文件的输出流
    OutputStream output = new FileOutputStream(file);

    Writer writer = new FileWriter(file);
  3. 把内容转换成字节数组
String str = "xxxx";
byte [] data = str.getBytes();

String str = "xxxx";
char[] data = str.toChararray();
  1. 向文件写内容
    output.write(data);writer.write(data);
  2. 关闭输入流
    output.close();

3. 随机读写文件

import java.io.File;
import java.io.RandomAccessFile;

public class RandomAccessFileTest {
    public static void main(String[] args) {
        File file = new File("D:\\Users\\80352170\\IdeaProjects\\CatchUp\\src\\hello_world.txt");
        /**
         * model各个参数详解
         * r 代表以只读方式打开指定文件
         * rw 以读写方式打开指定文件
         * rws 读写方式打开,并对内容或元数据都同步写入底层存储设备
         * rwd 读写方式打开,对文件内容的更新同步更新至底层存储设备
         */
        try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
            //获取RandomAccessFile对象文件指针的位置,初始位置是0
            System.out.println("RandomAccessFile文件指针的初始位置:"+raf.getFilePointer());
            //移动文件指针位置
            raf.seek(1);
            byte[]  buff=new byte[1024];
            //用于保存实际读取的字节数
            int hasRead=0;
            //循环读取
            while((hasRead=raf.read(buff))>0){
                //打印读取的内容,并将字节转为字符串输入
                System.out.println(new String(buff,0,hasRead));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4. 复制文件

拷贝的核心思想:从源文件读取数据,通过循环写入到目标文件中,从而实现文件复制。

import java.io.*;

public class FileCopyTest {
    public static void main(String[] args) {
        // 构建源文件
        File file = new File("D:\\Users\\80352170\\IdeaProjects\\CatchUp\\src\\Hello_world.txt");
        // 构建目标文件
        File fileCopy = new File("D:\\Users\\80352170\\IdeaProjects\\CatchUp\\src\\Hello_worldCopy.txt");
        InputStream in = null;
        OutputStream out = null;

        try{
            // 目标文件不存在就创建
            if(!(fileCopy.exists())){
                fileCopy.createNewFile();
            }

            //源文件创建输入流
            in = new FileInputStream(file);
            // 目标文件创建输出流
            out = new FileOutputStream(fileCopy,true);

            byte[] temp = new byte[1024];
            int len = 0;

            System.out.println("the output of in.read(temp) is " + in.read(temp));
            System.out.println("the output of in.read(temp) is " + in.read(temp));
            /* Output:
            * the output of in.read(temp) is 17
            * the output of in.read(temp) is -1
            * */
            
            while((len = in.read(temp)) != -1){

                // 源文件读取部分内容
                out.write(temp,0,len);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try{
                // 关闭输入和输出流
                in.close();
                out.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

注:在调试时,发现新的copy文件建立,但并未把内容复制进去。之后发现,是在调试时把 in.read(temp) 打印出来了,导致后面再调用 in.read(temp) 时,返回-1。
在查看 read() API时发现, public int read(byte[] b) throws IOException
从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。在某些输入可用之前,此方法将阻塞。
参数:
b - 存储读取数据的缓冲区。
返回:
读入缓冲区的字节总数,如果因为已经到达文件末尾而没有更多的数据,则返回 -1。

5. 读取文件信息

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.util.Date;

public class FiIeInfoDemo {
    public static void main(String[] args) throws IOException {
        Path testpath = Paths.get("D:\\Users\\80352170\\IdeaProjects\\CatchUp\\src\\Hello_world.txt");
        BasicFileAttributeView infoView = Files.getFileAttributeView(testpath,BasicFileAttributeView.class);
        BasicFileAttributes fileAttri = infoView.readAttributes();

        System.out.println("创建时间:"+ new Date(fileAttri.creationTime().toMillis()));
        System.out.println("最后访问时间:" + new Date(fileAttri.lastAccessTime().toMillis()));
        System.out.println("最后修改时间:" + new Date(fileAttri.lastModifiedTime().toMillis()));
        System.out.println("文件大小:" + fileAttri.size());

        FileOwnerAttributeView ownerView = Files.getFileAttributeView(testpath,FileOwnerAttributeView.class);
        System.out.println("文件所有者:" + ownerView.getOwner());
    }
}

3. 网络编程

网络编程是指编写运行在多个设备的程序,这些设备都通过网络连接起来。

java.net 包中J2SE 的API包含有类和接口,它们提供低层次的通信细节。开发者可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节。

java.net 包中提供了两种常见的网络协议的支持:

  • TCP:TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP层是位于IP层之上,应用层之下的中间层。TCP 保障了两个应用程序之间的可靠通信。通常用于互联网协议,被称TCP/IP。
  • UDP:UDP位于OSI模型的传输层。一个无连接的协议。提供了应用程序之间要发送数据的数据报。由于UDP缺乏可靠性,且属于无连接协议,所以应用程序通常必须容许一些丢失、错误或重复的数据包。

网络编程的基础主要讲解一下两个主题:
Socket编程:这是使用最广泛的网络概念,它已被解释地非常详细。
URL处理:将介绍Java是如何处理URL的。

Socket编程

Socket使用TCP提供了两台计算机之间的通信机制。
客户端程序创建一个Socket,并尝试连接服务器的Socket。
当连接建立时,服务器会创建一个Socket对象。客户端和服务器现在可以通过对Socket对象的写入和读取来进行通信

服务端
服务端的实现步骤如下:

  1. 创建一个 ServerSocket 对象,指定端口号。
  2. 使用 serverSocket 对象中的 accept() 方法,获取到请求的客户端 Socket
  3. 使用 Socket 对象中的 getInputStream 方法,获取到网络字节输入流 InputStream 对象。
  4. 使用 InputStream 对象中的 read 方法读取客户端发送的数据。
  5. 使用 Socket 对象中的 getOutputStream 方法,获取到网络字节输出流 OutputStream 对象。
  6. 使用 OutputStream 对象总的 write 方法,给客户端发送数据。
  7. 释放资源(关闭 SocketServerSocket )。

客户端
客户端的实现步骤如下:

  1. 创建一个客户端 Socket 对象,构造方法中绑定服务器的 IP 地址和端口号。
  2. 使用 Socket 对象中的 getOutputStream 方法,获取网络字节输出流 OutputStream 对象
  3. 使用 OutputStream 对象中的 write 方法,给服务器发送数据。
  4. 使用 Socket 对象中的 getInputStream 方法,获取网络字节输入流 InputStream 对象。
  5. 使用 InputStream 对象中的 read 方法读取服务器反馈的数据。
  6. 释放资源(关闭 Socket )。

连接建立后,通过使用IO流再进行通信,每一个socket都有一个输出流和一个输入流,客户端的输出流连接到服务端的输入流,而服务端的输出流连接到客户端的输入流。
TCP是一个双向的通信协议,因此数据可以通过两个数据流在同一时间发送

简单示例:
Socket客户端实例

import java.io.*;
import java.net.Socket;

public class SocketClientDemo {
    public static void main(String[] args) {
        String serverName = args[0];
        int port = Integer.parseInt(args[1]);
        try{
            System.out.println("连接到主机:" + serverName + "端口号:" + port);

            Socket client = new Socket(serverName,port);
            System.out.println("远程主机地址:" + client.getRemoteSocketAddress());

            OutputStream outToServer = client.getOutputStream();
            DataOutputStream out = new DataOutputStream(outToServer);

            out.writeUTF("Hello from " + client.getLocalSocketAddress());
            InputStream inFromServer = client.getInputStream();
            DataInputStream in = new DataInputStream(inFromServer);
            System.out.println("服务器响应:" + in.readUTF());

            client.close();
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

Socket服务端实例

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;

public class SocketServerDemo extends Thread{
    private ServerSocket serverSocket;

    public SocketServerDemo(int port) throws IOException{
        serverSocket = new ServerSocket(port);
        serverSocket.setSoTimeout(20000);
    }

    public void run(){
        while(true){
            try{
                System.out.println("等待远程连接,端口号为:" + serverSocket.getLocalPort() + "...");
                Socket server = serverSocket.accept();
                System.out.println("远程主机地址:" + server.getRemoteSocketAddress());
                DataInputStream in = new DataInputStream(server.getInputStream());
                System.out.println(in.readUTF());

                DataOutputStream out = new DataOutputStream(server.getOutputStream());
                out.writeUTF("谢谢你连接我:" + server.getLocalSocketAddress() + "\nGoodgbye!");
            }catch (SocketTimeoutException e){
                System.out.println("Socket time out! ");
//                e.printStackTrace();
                break;
            }catch (IOException s){
                s.printStackTrace();
                break;
            }
        }
    }

    public static void main(String[] args) {
        int port = Integer.parseInt(args[0]);
        try{
            Thread t = new SocketServerDemo(port);
            t.run();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

用命令来启动服务,使用端口号为6066:

$ javac SocketServerDemo.java 
$ java SocketServerDemo 6066
等待远程连接,端口号为:6066...

新建另一个命令窗口,执行以下命令来开启客户端:

$ javac SocketClientDemo.java 
$ java SocketClientDemo localhost 6066
连接到主机:localhost ,端口号:6066
远程主机地址:localhost/127.0.0.1:6066
服务器响应: 谢谢连接我:/127.0.0.1:6066
Goodbye!

同步和异步:

同步和异步是针对应用程序和内核的交互而言的。
同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪。
异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO 操作已经完成的时候会得到IO完成的通知。
以银行取款为例:
同步:自己亲自持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写);
异步:委托他人拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码)),OS需要支持异步API).

阻塞与非阻塞

阻塞和非阻塞是针对进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作方法的实现方式。
阻塞方式下,读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入方法会立即返回一个状态值。
非阻塞方式下,读取或者写入方法会立即返回一个状态值。
以银行取款为例:
阻塞:ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回);
非阻塞:柜台取款,取个号,然后坐在椅子上做其他事,等号广播会通知你办理,没到号你就不能去。你可以不断问排到了没有,如果还没到就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO时间分发器通知可读写时再继续进行读写,不断循环直到读写完成)

BIO编程

Blocking IO:同步阻塞的编程方式。
BIO编程方式通常是在JDK1.4版本之前常用的。编程实现过程为:首先在服务端启动一个ServerSocket 来监听网络请求,客户端启动Socket发起网络请求,默认情况下ServerSocket会建立一个线程来处理此请求,如果服务端没有线程可用,客户端则会阻塞等待或遭到拒绝。
且建立好的连接,在通讯过程中,是同步的。在并发处理效率上比较低。大致结构如下:

BIO结构

同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时,服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的开销,当然可以通过线程池机制改善。
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4前的唯一选择,但程序直观简单易理解。
使用线程池机制后的BIO结构

NIO编程:

Unblocking IO(New IO)=> 同步非阻塞编程编程方式

NIO本身是基于事件驱动思想来完成的,其主要思想是解决BIO的大并发问题,NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。

NIO的最重要的地方是当一个连接创建后,不需要对应一个线程。这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,返现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。


image

NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中。JDK1.4开始支持。

AIO编程

Asynchronous IO => 异步非阻塞编程方式
与NIO不同,当进行读写操作时,只需直接调用API的read或write方法即可。这两种方法均为异步的。对于读操作而言,当有流可读取时,操作系统会将可读的流传入read() 方法的缓冲区,并通知应用程序。即可以理解为,read()write() 方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包内。

异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是有OS先完成了,再通知服务器应用去启动线程进行处理。

AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。


AIO简易流程图

实战:

文件上传: 下面实现一个简单地文件上传程序。
服务端代码:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;

public class Server {

    public static void saveFile(Socket socket)  {
        try{
            InputStream inStream = socket.getInputStream();
            File file = new File("/tmp/upload/");
            if(!file.exists()){
                file.mkdirs();
            }

            String fileName = "copy_" + System.currentTimeMillis() + ".jpg";

            FileOutputStream foutStream = new FileOutputStream(file + "/" + fileName);

            byte[] bytes = new byte[1024];
            int len = 0;

            System.out.println("服务端开始向硬盘写入文件, 文件名: " + fileName);

            int i = 0;
            // 注意:必须写成 (len = inStream.read(bytes) != -1)
            // 如果在前面就声明好,len的值不会变,会陷入死循环
            while((len = inStream.read(bytes)) != -1){
                // 写入到新的文件中
                System.out.println("开始写入" + i++);

                foutStream.write(bytes,0,len);
            }

            System.out.println("服务器写入完毕! ");
            OutputStream outStream = socket.getOutputStream();
            // 发送给Client
            outStream.write("上传成功".getBytes());

            foutStream.close();
            outStream.close();
            socket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(8888);
        while(true){
            Socket socket = server.accept();
            new Thread(() -> saveFile(socket)).start();
        }
    }
}

客户端代码:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class Client {
    public static void main(String[] args) throws IOException {
        //创建本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("D:\\Users\\80352170\\IdeaProjects\\IMG_2309.jpg");
        //创建一个客户端Socket对象,构造方法中绑定服务器的IP地址和端口号
        Socket socket = new Socket("127.0.0.1", 8888);
        //使用Socket中的getOutputStream,获取网络字节输出流OutputStream对象
        OutputStream os = socket.getOutputStream();
        //使用本地字节输入流FileInputStream对象中的方法read,读取本地文件
        int len = 0;
        byte[] bytes = new byte[1024];
        while ((len = fis.read(bytes)) != -1){
            //使用网络字节输出流OutputStream对象中的方法write,把读取到的文件上传到服务器
            os.write(bytes, 0, len);
        }
        socket.shutdownOutput(); //关闭此套接字的输出流(这一步很重要,如果缺少了,服务器读取文件之后将进入阻塞状态)
        //使用Socket中的getInputStream,获取网络字节输入流InputStream对象
        InputStream is = socket.getInputStream();

        System.out.println("文件发送完毕!");

        //使用网络字节输入流InputStream对象中的方法read读取服务器回写的数据
        while((len = is.read(bytes)) != -1){
            System.out.println(new String(bytes, 0, len));
        }

        //释放资源(关闭FileInputStream、Socket)
        fis.close();
        socket.close();
    }
}

4. 正则表达式

正则表达式定义了字符串的模式。
正则表达式可以用来搜索、编辑或处理文本。
正则表达式不仅限于某一种语言,但是每种语言中有细微的差别。

正则表达式实例
一个字符串其实就是一个简单地正则表达式,例如Hello World正则表达式匹配“Hello World”字符串。下面列出了一些正则表达式的实例及描述:
this is text => 匹配字符串“this is text”
this\s+is\s+text => 注意字符串中的 \s+。匹配单词 “this” 后面的\s+可以匹配多个空格,之后匹配 “is” 字符串,再之后\s+匹配U盾讴歌空格然后再跟上“text”字符串。
^\d+(.\d+)? => ^定义了以什么开始。\d+匹配一个或多个数字。设置括号内的选项是可选的。.匹配"."。所以可以匹配的实例:“5”,“2.3”和“3.31”。
java.util.regex

匹配规则

正则表达式的匹配是从左到右来匹配的。

\\
在Java中,来对应字符串中的特殊字符例如 “a&c”,对应的Java字符串是 “a\\&c”

\u####
如果想匹配非ASCII字符,例如中文,那就用 \u####的十六进制表示,例如匹配字符串“a和c”,中文字符“”的Unicode编码是“548c”。

匹配任意字符:.
在Java中,精确匹配一般直接用 String.equals() 就可以做到。而大多数情况下,我们想要的匹配规则更多的是模糊匹配。我们可以用 . 来匹配一个任一字符。
例如,正则表达式 a.c 就可以匹配 abc, a&c, acc等。注意,但不能匹配 ac或者 a&&c 是因为 . 有且只能匹配一个字符。

匹配数字:\\d
如果我们只想匹配 0~9 这样的数字,可以用 \\d 匹配。例如,正则表达式 00\\d 可以匹配 “007”,“009”。但是不能匹配“0077”,因为 \\d 仅限单个数字字符

匹配常用字符:\\w
\\w 可以匹配一个字母、数字或下划线。例如, java\\w 可以匹配“javac”、“java8”或“java_”。但注意它不能匹配空格、“#”等字符。

匹配空格字符:\\s
\\s 可以匹配一个空格字符,并且可以匹配一个tab字符(在Java中用 \\t 表示)。例如, a\\sc 可以匹配 “a c”或者 “a c”。但不匹配 “ac”,“abc”等。

匹配非数字:\\D
\\D 是用来匹配一个非数字。例如, 00\\D 可以匹配 “00A”,“00#”。
类似的, \\W 可以匹配 \\w 不能匹配的字符, \\S 可以匹配 \\s 不能匹配的字符。

重复匹配
修饰符 * 可以匹配任一个字符,包括0个字符
修饰符 +可以匹配至少一个字符
修饰符 ? 可以匹配0个或1个字符
修饰符 {n} 可以精确匹配n个字符。例如, A\d{3} 可以精确匹配 A380 等后面接3个数字。
修饰符 {n,m} 可以匹配 n~m个字符。

实例: 编写一个正则表达匹配国内的电话号码规则 3~4位区号 - 7~8位电话

public class RegexDemo {
    public static void main(String[] args) {
        String re = "\\d{3,4}\\-\\d{7,8}";
        String[] str1 = {"010-12345678", "020-9999999", "0755-7654321","010 12345678", "A20-9999999", "0755-7654.321"};
        for(String s: str1){
            if(s.matches(re)){
                System.out.println("测试成功:" + s);
            }else {
                System.out.println("测试失败:" + s);
            }
        }
    }
}

复杂匹配规则

匹配指定范围
[...] 可以匹配范围内的字符。 ... 可以具体情况具体填写。
例如 [1-9] 可以匹配 1~9内的任意一个数字。
如果要匹配大小写不限的字母或数字,则可以这样表达 [0-9a-zA-Z]
还有一种是排除法,即不包含指定范围的字符,例如不包括3个数字,则可以写成 [^1-9]{3} ,可以匹配例如 “ABC”,“A00”,但不可以匹配“A01”等,因为包含“1”。

或规则匹配
| 连接的两个正则表达是或规则。例如 java|php|python 则可以匹配 “java”,“php”和“python”,然而“go”则不行。

使用括号
如果想要匹配 learn javalearn phplearn go ,用 learn\sjava|learn\sphp|learn\sgo 来表达太复杂了。可以把公共部分提出来然后用括号表示,learn\\s(java|php|go)

分组匹配

分组匹配是用 ( ) 把要提取的规则分组,然后通过 java.util.regex 包,用 Pattern 对象匹配,匹配后获得一个 Matcher 对象,如果匹配成功,就可以直接从 Matcher.group(index) 返回字符串:

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexDemo {
    public static void main(String[] args) {
        String re = "(\\d{3,4})\\-(\\d{7,8})";
        Pattern p = Pattern.compile(re);
        Matcher m = p.matcher("020-9999999");
        if(m.matches()){
            String str1 = m.group(0);
            String str2 = m.group(1);
            String str3 = m.group(2);
            System.out.println(str1);
            System.out.println(str2);
            System.out.println(str3);
        }else {
            System.out.println("暂无匹配!");
        }
    }
}

output:

020-9999999
020
9999999

运行上述代码,group(0) 是整个正则匹配到的字符串,1就是第一个子串,以此类推,注意不要出现 超出边界。

Pattern
之前匹配都是调用 String.matches() 方法,而在上述代码中调用的是 java.util.regex 包里面的 Pattern 类和 Matcher 类。实际上这两种调用本质上是一样的,因为 String.matches() 方法内部调用的就是 Pattern 类和 Matcher 类的方法。

但是反复调用 String.matches() 对同一个正则表达式进行多次匹配效率较低,因为每次都会创建出一样的 Pattern 对象。 完全可以先创建一个 Pattern 对象,然后哦反复使用,就可以实现编译一次,多次匹配。

public class Main {
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("(\\d{3,4})\\-(\\d{7,8})");
        pattern.matcher("010-12345678").matches(); // true
        pattern.matcher("021-123456").matches(); // true
        pattern.matcher("022#1234567").matches(); // false
        // 获得Matcher对象:
        Matcher matcher = pattern.matcher("010-12345678");
        if (matcher.matches()) {
            String whole = matcher.group(0); // "010-12345678", 0表示匹配的整个字符串
            String area = matcher.group(1); // "010", 1表示匹配的第1个子串
            String tel = matcher.group(2); // "12345678", 2表示匹配的第2个子串
            System.out.println(area);
            System.out.println(tel);
        }
    }
}

使用 Matcher 时,必须首先调用 matches() 判断是否匹配成功,匹配成功后,才能调用 group() 提取子串。

非贪婪匹配

如果想要判定“123000”,“10100”,“10101”数字的末尾“0”的个数,如果单纯通过 (\d+)(0*) 来表达,则会得到 “group1=123000”和“group2= ”的结果。很显然与期望不符,但确实合理的,因为 \d+ 确实可以匹配后面任意个数字。
所以这就是正则表达默认使用的贪婪匹配:任何一个规则,它总是尽可能多地向后匹配。因此, \d+ 总是会把后面的0包含进来。

要让 \d+ 尽量少匹配,就必须在它之后加个 ? 表示非贪婪匹配。
所以正则表达式可以改成: (\\d+?)(0*)

再来看这个正则表达式 (\d??)(9*) ,注意这里的 \d? 表示匹配0个或1个数字,后面第二个 ? 表示非贪婪匹配。因此,给定字符串 “9999”,返回的两个子串分别为 “”和“9999”。因为对于 \d? 来说,可以匹配1个9,也可以匹配0个9,但是因为后面的 ? 表示非贪婪匹配,它就会尽可能少的匹配,结果是匹配了0个9。

搜索和替换

分割字符串

"a b c".split("\\s"); // { "a", "b", "c" }
"a b  c".split("\\s"); // { "a", "b", "", "c" }
"a, b ;; c".split("[\\,\\;\\s]+"); // { "a", "b", "c" }

如果我们想让用户输入一组标签,然后把标签提取出来,因为用户的输入往往是不规范的,这时,使用合适的正则表达式,就可以消除多个空格、混合 , 和 ; 这些不规范的输入,直接提取出规范的字符串。

搜索字符串
Matcher.matches() 对整个字符串进行匹配,只有整个字符串都匹配了才返回true
Matcher.find() 对字符串进行匹配,匹配到的字符串可以在任何位置
Mathcer.start()/ Matcher.end()返回匹配到的子字符串的第一个字符/最后一个字符在字符串中的索引位置。

public class Main {
    public static void main(String[] args) {
        String s = "the quick brown fox jumps over the lazy dog.";
        Pattern p = Pattern.compile("\\wo\\w");
        Matcher m = p.matcher(s);
        while (m.find()) {
            String sub = s.substring(m.start(), m.end());
            System.out.println(sub);
        }
    }
}
//output:
//row
//fox
//dog

替换字符串
使用正则表达式替换字符串可以直接调用 String.replaceAll() ,它的第一个参数是正则表达式,第二个参数是待替换的字符串。我们还是来看例子:

public class Main {
    public static void main(String[] args) {
        String s = "The     quick\t\t brown   fox  jumps   over the  lazy dog.";
        String r = s.replaceAll("\\s+", " ");
        System.out.println(r); // "The quick brown fox jumps over the lazy dog."
    }
}
// output: The quick brown fox jumps over the lazy dog.

上面的代码把不规范的连续空格分隔的句子变成了规范的句子。可见,灵活使用正则表达式可以大大降低代码量。

5. 反射机制

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。反射机制在框架设计中极为广泛。

反射基础

RTTI(Run-Time Type Identification)运行时类型识别,其作用是在运行时,识别一个对象的类型和类的信息。主要有两种方式:一种是“传统的”RTTI,它假定我们在编译时已经知道了所有的类型;另一种是“反射”机制,它允许我们在运行时发现和使用类的信息。

反射就是把Java的各种成分映射成一个个的Java对象。
例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。

首先需要理解Class类,以及类的加载机制;然后基于此我们如何通过反射获取Class类以及类中的成员变量、方法、构造方法等。

Class类

Class类也是一个实实在在的类,存在于JDK的 java.lang 包中。 Class类的实例表示Java应用运行时的类(class ans enum)或接口(interface and annotation)(每个Java类运行时都在JVM里表现为一个class对象)。数组同样也被映射为class对象的一个类,所有具有相同元素类型和维数的数组都共享该Class对象。基本类型boolean,byte,char,short,int,long,float,double和关键字void同样表现为class对象。

小结:

  • Class类也是类的一种,与class关键字是不一样的。
  • 手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且这个Class对象保存在同名 .class 的文件中(字节码文件)
  • 每个通过关键字 class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象。
  • Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载。
  • Class类的对象作用是运行时提供或获得某个对象的类型信息,这点对于反射技术很重要。

类加载

类加载机制流程:


image

类的加载:


image

反射的使用

在Java中,Class类与java.lang.reflect类库一起对反射技术进行了支持。在反射包中,我们常用的类主要有Constructor类表示的是Class对象所表示的类的构造方法,利用它可以在运行时动态创建对象、Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)、Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private),下面将对这几个重要类进行分别说明。

Class类对象的获取

一个简单的例子:

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class Apple {
    private int price;
    public int getPrice(){
        return price;
    }

    public void setPrice(int price){
        this.price = price;
    }

    public static void main(String[] args) throws Exception {
        // 正常的调用
        Apple apple = new Apple();
        apple.setPrice(4);
        System.out.println("the price is "+ apple.getPrice());

        //使用反射的调用
        Class clz = Class.forName("Apple");
        Method setPriceMethod = clz.getMethod("setPrice", int.class);
        Constructor appleConstructor = clz.getConstructor();
        Object appleObj = appleConstructor.newInstance();
        setPriceMethod.invoke(appleObj,14);
        Method getPriceMethod = clz.getMethod("getPrice");
        System.out.println("the price is " + getPriceMethod.invoke(appleObj));
    }
}

从代码中可以看到,我们使用了反射调用了 setPrice 方法,并传递了 14 的值。之后使用反射调用了 getPrice 方法,输出其价格。上面的代码整个的输出结果是:

the price is 4
the price is 14

从这个简单的例子可以看出,一般情况下,我们使用反射获取一个对象的步骤:

  • 获取类的 Class 对象实例
    Class clz = Class.forName("Apple");
  • 根据Class对象实例获取 Constructor 对象
    Constructor appleConstructor = clz.getConstructor();
  • 使用 Constructor 对象的 newInstance 方法获取反射对象
    Object appleObj = appleConstructor.newInstance();
    而如果要调用某一个方法,则需要经过下面的步骤:
  • 获取方法的 Method 对象
    Method setPriceMethod = clz.getMethod("setPrice", int.class);
  • 利用 inVoke 方法调用方法
    setPriceMethod.invoke(appleObj, 14);
    至此,这些事对反射的基本使用,但如果要进一步掌握反射,还需要对反射的常用API有更深入的理解。

在JDK中,反射相关的API可以分为下面几个方面

  • 获取反射的Class对象
  • 通过反射创建类对象
  • 通过反射获取类属性方法及构造器

反射常用API

获取反射中的Class对象

在反射中,要获取一个类或调用一个类的方法,我们首先需要获取到该类的Class对象。
在Java API中,获取Class类对象有三种方法:

  • 使用 Class.forName 静态方法
    当知道该类的全路径名时,可以使用该方法获取对象。
    Class clz = Class.forName("java.lang.String");
  • 使用 .class 方法
    这种方法只适合在编译前就知道操作的Class。
    Class clz = String.class;
  • 使用类对象的 getClass() 方法
String str = new String("Hello");
Class clz = str.getClass();

通过反射创建类对象

通过反射创建类对象主要有两种方式:

  • 通过Class对象的 newInstance()
Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();
  • 通过 Constructor 对象的 newInstance()
Class clz = Apple.class;
Constructor constructor = clz.getConstructor();
Apple apple = (Apple)constructor.newInstance();

通过 Constructor 对象创建类对象可以选择特定构造方法,而通过Class对象则只能使用默认的无参数构造方法。下面代码就调用了一个有参数的构造方法进行了类对象的初始化。

Class clz = Apple.class;
Constructor constructor = clz.getConstructor(String.class, int.class);
Apple apple = (Apple)constructor.newInstance("红富士", 15);

通过反射获取类属性、方法、构造器

通过Class对象的 getFields() 方法可以获取 Class 类的属性,但无法获取私有属性

// private int price;
// public String name;
Class clz = Apple.class;
Field[] fields = clz.getFields();
for (Field field : fields) {
   System.out.println(field.getName());
}
// Output: name

而使用 Class 对象的 getDeclaredField() 方法则可以获取包括私有属性在内的所有属性

// private int price;
// public String name;
Class clz = Apple.class;
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
   System.out.println(field.getName());
}
//Output: price
 //       name

与获取类属性一样,当我们获取类方法、类构造器时,如果要获取私有方法或私有构造器,则必须使用有 declared 关键字的方法。

6. 垃圾回收机制

垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。
注意:GC回收的是无任何引用的对象占据的内存空间,而不是对象本身。换言之,垃圾回收只会负责释放那些对象占有的内存。对象是个抽象的词,包括引用和其占据的内存空间。当对象没有任何引用时其占据的内存空间随即被收回备用。此时对象也就被销毁,但说回收对象并不恰当。

分析:
引用:如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。(引用都有哪些?对GC又有什么影响?

垃圾:无任何对象引用的对象(如何通过算法找到这些对象?

回收:清理“垃圾”占用的内存空间而非对象本身(如何通过算法实现回收?

发生地点:一般发生在堆内存中,因为大部分的对象都存储在堆内存中(堆内存为了配合GC有什么不同区域划分,各个区域有什么不同

image

发生时间:程序空闲时间不定时回收(回收机制是什么?是否可以通过显示调用函数方式来确定的进行回收过程

Java中的对象引用

  • 强引用:Object obj = new Object() ,这类引用时Java程序中最普遍的。只要强引用还存在,垃圾回收器就永远不会回收掉被引用的对象

  • 软引用:它用来描述一些可能还有用,但并非必须的对象。在系统内存不够时,这类引用关联的对象将被垃圾回收器回收。JDK1.2 之后提供了SoftReference类来实现软引用。

  • 弱引用:它也是用来描述非须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了 WeakReference类来实现弱引用。

  • 虚引用:最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被回收时收到一个系统通知。 JDK1.2之后提供了 PhantomReference类来实现虚引用。

对象与引用的区别:

对象:按照通俗的说法,每个对象都是某个类(class)的一个实例(instance),这里,“类”就是“类型”的同义词。

引用:操纵的标识符实际是指向一个对象的“引用”。

通常会用下面这一行代码来创建一个对象:
Person person = new Person("张三");
在看下面两行代码:

Person person; // 引用
person = new Person("张三"); // 对象

这两行代码实现的功能和上面的一行代码是完全一样的。在Java中 new 是用来在堆上创对象用的。如果 person 是一个对象的话,那么第二行为何还要通过 new 来创建对象呢?由此可见, person 并不是创建的对象。上面一段话说的很清楚,“操纵的标识符实际是指向一个对象的引用”,也就是说, person 是一个引用,是指向一个可以指向Person类的对象的引用。 真正创建对象的语句是右边的 new Person("张三") 。再看一个例子:

Person person;
person = new Person("张三");
person = new Person("李四");

这里让 person 先指向了“张三”这个对象,然后又指向了“李四”这个对象。也就是说, Person person 这句话只是声明了一个 Person类的引用,它可以指向任何Person类的实例。
另外,一个引用可以指向多个对象,一个对象也可以被多个引用所指。

Person person1 = new Person("张三");
Person person2 = person1;

判断对象是否是垃圾的算法

Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件事情:(1)找到所有存活的对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。

引用计数算法(Reference Counting Collector)

堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量技术设置为1。每当有一个地方引用它时,计数器值就加1(a=b,b被引用,则b引用的对象计数+1)。当引用失效时(一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时),计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。

优点:引用计数收集齐执行简单,判定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利(Objective-C的内存管理使用该方法)。

缺点: 难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以Java语言并没有选择这种算法进行GC。

早起的JVM使用引用计数,现在大多数JVM采用对象引用遍历(根搜索算法)。

根搜索算法(Tracing Collector)

首先了解一个概念:

根集(Root Set):所谓根集就是正在执行的Java程序可以访问的引用变量的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。
这种算法的基本思路:

  • (1)通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
  • (2)找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
  • (3)重复(2)。
  • (4)搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。

标记可达对象:
JVM中用到的所有现代GC算法在回收前都会先找出所有仍存活的对象。根搜索算法是从离散数学中的图论引入的。程序把所有的引用关系看做一张图。下图中展示的JVM中的内存布局可以较好地阐释这一概念:


首先,垃圾回收器将某些特殊的对象定义为GC根对象。所谓GC根对象包括:

  • (1)虚拟机栈中引用的对象(栈帧中的本地变量表);
  • (2)方法区中的常量引用的对象;
  • (3)方法区中的类静态属性引用的对象;
  • (4)本地方法栈中JNI(Native方法)的引用对象
  • (5)活跃线程
    接下来,垃圾回收器会对内存中的整个对象图进行遍历,它先从GC根对象开始,然后是根对象引用的其它对象,比如实例变量。回收器将访问到的所有对象都标记为存活。

存活对象在上图中被标记为蓝色。当标记阶段完成了之后,所有的存活对象都已经被标记完了。其他的那些(上图中灰色的那些)也就是GC根对象不可达的对象,就是不会再用到它们这些就是垃圾对象。回收器将会在接下来的阶段中清除它们。

关于标记阶段中值得注意的点:

  • (1)开始进行标记前,需要先暂停应用线程,否则如果对象图一直在变化是无法真正去遍历它的。暂停应用线程以便JVM可以去遍历统计的情况又被称为安全点(SafePoint),这会触发一次Stop The World(STW)暂停。触发安全点的原因有许多,但最常见的应该就是垃圾回收了。

  • (2)暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。

  • (3)在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:

  1. 如果对象在进行根搜索后发现没有GC Roots相连接的引用链,那它会被第一次标记并且进行筛选。筛选的条件是此对象是否有必要执行 finalize() 方法(可看作析构函数)。当对象没有覆盖 finalize() 方法或 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
  2. 如果该对象被判定为有必要执行 finalize() 方法,那么这个对象将会被放置在一个名为 F-Queue 队列中,并在稍后被一条由虚拟机自动建立的、低优先级的 Finalizer 线程去执行 finalize() 方法。 finalize() 方法是对象逃脱死亡命运的最后一次机会(因为一个对象的 finalize() 方法最多只会被系统自动调用一次),稍后GC将对 F-Queue 中的对象进行第二次小规模的标记,如果要在 finalize() 方法中成功拯救自己,只要在 finalize() 方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
  • 实际上GC判断对象是否可达,看的是强引用。
    当标记阶段完成后,GC开始进入下一阶段,删除不可达对象。

回收垃圾对象内存的算法

Tracing算法(Tracing Collector)/标记-清除算法

标记-清除算法是最基础的收集算法,为了解决引用计数法的问题而提出。它使用了根集的概念,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。

优点:不需要进行对象的移动,并且仅对不存活的对象 进行处理,在存活对象比较多的情况下极为高效。
缺点:(1)标记和清除过程的效率都不高。(这种方法需要使用一个空闲列表来记录所有的空闲区域及大小,对空闲列表的管理会增加分配对象时的工作量)


(2)标记清除后会产生大量不连续的内存碎片。虽然空闲区域的大小是足够的,但却可能没有一个单一区域能够满足这次分配所需的大小,因此本次分配还是会失败,不得不触发另一次垃圾收集动作。

Compacting算法(Compacting Collector)/标记-整理算法

该算法标记的过程与标记-清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于 Compacting 算法的收集器的实现中,一般增加句柄和句柄表。



优点:(1)经过整理后,新对象的分配只需要通过指针碰撞便能完成(Pointer Bumping),相当简单
(2) 使用这种方法空闲区域的位置始终可知的,也不会有碎片问题了

缺点: GC暂停时间会增长,因为需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。

Copying算法(Copying Collector)

该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。

复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记-整理算法。一种典型的基于 Copying 算法的垃圾回收是 stop-and-copy 算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。



优点:
(1)标记阶段和复制阶段可以同时进行。
(2)每次只对一块内存进行回收,运行高效
(3)只需移动栈顶指针,按顺序分配内存即可,实现简单
(4)内存回收时,不用考虑内存碎片的问题

缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存少了一半。

Adaptive算法(Adaptive Collector)

在特定的情况下,一些垃圾算法会优于其他算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。

Java的堆内存

Java的堆内存基于Generation算法(Generational Collector)划分为新生代、年老代和持久代。新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成。所有通过new创建的对象内存都在堆中分配,其大小可以通过 -Xmx 和 -Xms来控制。

分代收集,是基于事实:不同的对象的生命周期是不一样的。因此,可以将不同声明周期的对象分代,不同的代采取不同的回收算法(4.1-4.3)进行垃圾回收(GC),以便提高回收效率。
堆内存分区示意图:


Java Heap Memory
Java Heap Memory2

Java的内存空间除了堆内存还有其他部分:
(1)栈
每个线程执行每个方法的时候,都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果。
(2)本地方法栈
用于支持Native方法的执行,存储了每个Native方法调用的状态。
(3)方法区
存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用持久代(Permanent Generation)来存放方法区,可通过 -XX:PermSize和 -XX:MaxPermSize来指定最小值和最大值。

堆内存分配区域:

年轻代(Young Generation)
几乎所有新生成的对象首先都是放在年轻代的。年轻代内存按照 8:1:1比例分为一个Eden区和两个Survivor区(Survivor0,Survivor1)。大部分对象在Eden区中生成。当新对象生成,Eden Space申请失败(空间不足等),则会发起一次GC(Scavenge GC)。回收时先将Eden区存活对象复制到一个Survivor0区,然后清空Eden区,当这个Survivor0区也存放满了时,则将Eden区和Survivor0区存活对象复制到另一个Survivor1区,然后清空Eden和Survivor0。然后将Survivor0和Survivor1交换,即保持Survivor1为空,如此往复。当Survivor1不足以存放Eden和Survivor0的存活对象时,就将存活对象直接存放到年老代。当对象在Survivor躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15,就会移动到年老代中。若是年老代也满了,就会触发一次Full GC,也就是年轻代和年老代都进行回收。年轻代大小可以由 -Xmn 控制,也可以用 -XX:SurvivorRatio 来控制Eden和Survivor的比例。

年老代(Old Generation)
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。内存比年轻代也大很多(大概1:2),当年老代内存满时触发 Full GC,Full GC发生频率比较低,年老代对象存活时间比较长,存活率标记高。一般来说,大对象会被直接分配到年老代。所谓的大对象是需要大量连续存储空间的对象,最常见的一种大对象就是大数组,例如:
byte[] data = new byte[4*1024*1024]
这种一般会直接在年老代分配存储空间。
当然分配的规则并不是百分百固定的,这要取决于当前使用的是哪种垃圾收集器组合和JVM相关参数。

持久代(Permanent Generation)
用于存放静态文件(class类、方法)和常量等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一下 class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。对持久代回收主要回收两部分内容:废弃常量和无用的类。

持久代空间在Java SE8特性中已经被移除。取而代之的是元空间(MetaSpace)。因此不会再出现 java.lang.OutOfMemoryError: PermGen error 错误。

堆内存分配策略明确一下三点:

  • 对象优先在Eden分配
  • 大对象直接进入年老代
  • 长期存活的对象将进入年老代

对垃圾回收机制说明:

年轻代GC(Minor GC/Scavenge GC):发生在年轻代的垃圾收集动作。因为Java对象大多都具有朝生夕灭的特性,因此Minor GC非常频繁(不一定等Eden区满了才触发),一般回收速度也比较快。在年轻代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制法来完成收集。

年老代GC(Major GC/Full GC): 发生在年老代的垃圾回收动作。Major GC经常会伴随至少一次Minor GC。由于年老代中的对象生命周期比较长,因此Major GC并不频繁,一般都是等待年老代满了后才进行Full GC,而且其速度一般会比Minor GC慢10倍以上。另外,如果分配了Direct Memory,在年老代中进行Full GC时,会顺便清理掉Direct Memory中的废弃对象。而年老代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记-清理算法”或“标记-整理算法”来进行回收。

年轻代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象咋年轻代区间的位置,当有新的对象要分配内存时,用于检查口空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到年老代。

用Java VisualVM来查看,能明显观察到年轻代满了后,会把对象转移到年老代,然后清空继续装载。当年老代也满了后,就会报 OutOfMemory 异常,如下图所示:

你可能感兴趣的:(Java知识回顾总结(3))