JavaIO流

前言

1. 基本知识

File类的对象可以针对文件或者文件夹

File类只能对文件本身进行操作,比如删除文件,获取文件大小,文件名信息等,但不能读写里面的数据

IO流就是用来读写文件的,还可以读写网络中的数据

右键,属性,安全,就可以看到一个文件的位置信息了

java 复制代码
public class test {
    public static void main(String[] args) {
        File file=new File("D:\\a.txt");//指定路径
        System.out.println(file.length());//文件大小、、字节
    }
}
java 复制代码
        File file=new File("D:/a.txt");
        System.out.println(file.length());

或者这样写也可以

java 复制代码
        File file=new File("D:"+File.separator+"a.txt");
        System.out.println(file.length());

或者这样

separator就是分隔符的意思

文件大小,就是里面装的内容的大小

如果是文件夹,就是里面装的文件的大小和,不是所有内容和

如果指定的是一个不存在的路径,那么求出来的大小就是0

java 复制代码
        File file=new File("D:"+File.separator+"a.txt");
        System.out.println(file.length());

        System.out.println(file.exists());//true

exists就是看这个路径的文件是否存在

java 复制代码
        File file=new File("test1021_1\\src\\a.txt");
        System.out.println(file.length());
        System.out.println(file.exists());//true

对于同一个项目里面的文件,可以使用相对路径,默认是在项目里面找的

所以相对路径的第一个就是模块名,而不是盘符

2.文件方法

java 复制代码
        File file=new File("test1021_1\\src\\a.txt");
        System.out.println(file.length());
        System.out.println(file.exists());
        System.out.println(file.isFile());//看是不是文件
        System.out.println(file.isDirectory());//看是不是文件夹
        System.out.println(file.getName());//获取文件名称
        long time=file.lastModified();//获取文件的最后修改时间,时间戳
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        System.out.println(sdf.format(time));
java 复制代码
        File file1=new File("test1021_1\\src\\a.txt");
        File file2=new File("D:\\\\a.txt");
        System.out.println(file1.getPath());//获取创建文件对象时,使用的路径
        System.out.println(file2.getPath());
java 复制代码
        System.out.println(file1.getAbsoluteFile());
        System.out.println(file2.getAbsoluteFile());//获取绝对路径
java 复制代码
        File f1=new File("D:/b.txt");//不存在这个路径
        System.out.println(f1.createNewFile());//创建一个新文件(文件内容为空),创建成功返回true
java 复制代码
        File f2=new File("D:/bbb");//不存在这个路径
        System.out.println(f2.mkdir());//创建文件夹,只能创建一级文件夹
        File f3=new File("D:/bbb/ccc/ddd");//不存在这个路径
        System.out.println(f3.mkdir());//创建文件夹,只能创建一级文件夹


java 复制代码
        File f3=new File("D:/bbb/ccc/ddd");//不存在这个路径,存在这个路径也不能创建
        System.out.println(f3.mkdirs());//创建文件夹,创建多级文件夹
java 复制代码
        System.out.println(f1.delete());//删除文件或者空文件夹,非空文件夹不能删除,然后就是删除后不会进入回收站
java 复制代码
        File f3=new File("D:/aaa");
        String[] names=f3.list();//会把下一级的文件名,或者文件,弄到一个字符串数组中
        for(String name:names)//范围for
        {
            System.out.println(name);
        }


java 复制代码
        File f3=new File("D:/aaa");
        File[] files=f3.listFiles();//会把下一级的文件夹,或者文件,弄到一个文件对象数组中
        for(File file:files)//范围for
        {
            System.out.println(file.getAbsoluteFile());
        }

当aaa是文件的时候,返回null

当aaa是空文件夹的时候,返回长度为0的数组

返回的数组也会有隐藏文件

如果没有权限访问aaa文件夹,返回null

3. 方法递归

java 复制代码
    public  static void test1()
    {
        test1();
    } 

这个是直接递归

java 复制代码
    public  static void test1()
    {
        test2();
    }

    public  static void test2()
    {
        test1();
    }

这个是间接递归

3.1文件搜索

在D盘中,找到QQ.exe这个文件,然后输出其位置

1.先找出D盘下所有一级文件对象

2.遍历所有一级文件对象,判断是否为文件

3.如果是文件,判断是否是自己想要的

4.如果是文件夹,需要继续进入到该文件夹,重复上面这个过程

java 复制代码
    /**
     *
     * @param dir /**右键就可以得到  目录
     * @param fileName 要搜索的文件名称
     */
        public static void searchFile(File dir,String fileName)
        {
            //先看存不存在
            if(!dir.exists()||dir==null)
            {
                return;
            }

            //首先看是不是文件夹
            if(dir.isFile())
            {
                if(dir.getName().contains(fileName)){
                    System.out.println("找到了"+dir.getPath());
                    return;
                }
                return;
            }

            //是文件夹
            File[] files=dir.listFiles();

            if(files!=null&&files.length>0)
            {
                for(File f:files)
                {
                    searchFile(f,fileName);
                }
            }

        }

那么删除非空文件夹也是类似的思想

也是递归的思想

4. 编码

4.1 基本知识

ASCII编码

就是一个字节存储一个字符,最高位为0

对于美国人完全够用了

但是我们汉字多

我们用的是GBK编码,就是一个中文字符编码成两个字节形式储存

还要注意的是GBK是包含了ASCII的

但是汉字的最高位必须是1,不然怎么区分哪个是英语字符,哪个是汉字字符

比如"我a你"

就这样

1xxxxxxx xxxxxxxx 0xxxxxxx 1xxxxxxx xxxxxxxx

然后又有Unicode字符集,是万国码,针对所有国家的

UTF-32 表示4个字节为一个字符,但是这样太浪费了

UTF-8编码,可变长编码

英文,数字,用ASCII

汉字三个字符

a 97 01100001

我 25105 110 001000 01001

m 109 01101101

0xxxxxxx ASCII

110xxxxx 10xxxxxx

1110xxxx 10xxxxxx 10xxxxxx

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

填充时,就填充xxxxxxx,其余的都是固定的

比如"a我m"

01100001 1110xxxx 10xxxxxx 10xxxxxx 01101101

将110 001000 01001填上去就可以了

开发人员用的都是UTF-8,编码不一样,就会出现乱码

英文和数字不会乱码,因为都包含了ASCII

这里可以更改编码方式

比如我们用GBK写a我m

转化为UTF-8,就变成这个样子了

分析一下

GBK中a我m为

0xxxxxxx 1xxxxxx xxxxxxx 0xxxxxxx

UTF-8中

第一个字节认识

第二个不认识所以为?

4.2 编码与解码

java 复制代码
        String data="a我b";
        byte[] bytes=data.getBytes();//默认以平台的字符集编码  将a我b编码成二进制,然后一个一个字节存在里面,存放在数组中
        System.out.println(Arrays.toString(bytes));
java 复制代码
        String data="a我b";
        byte[] bytes=data.getBytes("GBK");
        System.out.println(Arrays.toString(bytes));

这样就是指定的

java 复制代码
        //解码
        String s1=new String(bytes);//将二进制的字节的数组解码成字符串,也是默认的平台解码
        System.out.println(s1);
java 复制代码
        //解码
        String s1=new String(bytes,"GBK");//这样就可以指定
        System.out.println(s1);

5.IO字节流

对于内存来说,数据到了磁盘或网络,叫输出

数据到了自己,叫输入

所以总共有四类

字节输入流,字节输出流,字符输入流,字符输出流

5.1文件字节输入流

java 复制代码
        //1.创建文件字节输入流,与源文件接通
        InputStream is=new FileInputStream(new File("E:\\java\\test1021_1\\src\\a.txt"));
        FileInputStream is=new FileInputStream("E:\\java\\test1021_1\\src\\a.txt");
        InputStream is=new FileInputStream("test1021_1\\src\\a.txt");

这三种构造方法都可以,但我们一般选择最后一种就可以了

java 复制代码
        int b1=is.read();//读一个字节到int中,读的是字节大小
        System.out.println(b1);
        System.out.println((char)b1);
java 复制代码
        int b1=is.read();//读一个字节到int中,读的是字节大小
        System.out.println((char)b1);

        int b2=is.read();//读一个字节到int中,读的是字节大小
        System.out.println((char)b2);

        int b3=is.read();//读一个字节到int中,读的是字节大小
        System.out.println((char)b3);

        int b4=is.read();//读一个字节到int中,读的是字节大小
        System.out.println(b4);

多来几次就会发现当没有数据读的时候,那么就会返回-1

所以可以优化一下

java 复制代码
        int b1=0;
        while((b1=is.read())!=-1)
        {
            System.out.print((char)b1);
        }

但是这有缺点,就是读取慢,一个一个读,读一次,就是一次系统调用,所以很麻烦

然后就是读到汉字的时候,就会出现乱码,因为只读一个字节


java 复制代码
        is.close();

使用完毕以后要关闭,释放系统资源

java 复制代码
        byte[] buffer=new byte[3];//读取的时候还可以一个一个数组的读  这个就表示一次读取三个字节
        int len=is.read(buffer);//返回的是读取的字节个数
        String rs=new String(buffer);
        System.out.println(rs);
        System.out.println(len);


java 复制代码
        byte[] buffer=new byte[3];//读取的时候还可以一个一个数组的读  这个就表示一次读取三个字节
        int len=is.read(buffer);//返回的是读取的字节个数
        String rs=new String(buffer);
        System.out.println(rs);
        System.out.println(len);

        len=is.read(buffer);//返回的是读取的字节个数
        rs=new String(buffer);
        System.out.println(rs);
        System.out.println(len);

        len=is.read(buffer);//返回的是读取的字节个数
        rs=new String(buffer);
        System.out.println(rs);
        System.out.println(len);

可以看出,没有数据读的时候,返回的读取个数就是-1

还有就是,对于同一个buffer来说,每次都是去覆盖的,所以会把上次的都打印了

优化一下

java 复制代码
       len=is.read(buffer);//返回的是读取的字节个数
        rs=new String(buffer,0,len);
        System.out.println(rs);
        System.out.println(len);

rs这样构造就不会打印错误了

注意len为-1,不能去构造,我没有构造的

java 复制代码
        byte[] buffer=new byte[3];
        int len;
        while((len=is.read(buffer))!=-1){
            String rs=new String(buffer,0,len);
            System.out.println(rs);
        }

这个就是循环读取的方法

但是这个也不能避免汉字乱码问题,因为是读一次打印一次

避免乱码的话,可以定义一个和文件一样大的数组,一次性读完

java 复制代码
        InputStream is=new FileInputStream("test1021_1\\src\\a.txt");
        File f=new File("test1021_1\\src\\a.txt");
        long size=f.length();//length返回的是long,而字节数组里面的大小为int,所以改一下,,这个length()返回的就是文件里面的字符个数
        byte[] buffer=new byte[(int)size];
        int len=is.read(buffer);

        System.out.println(new String(buffer));
        System.out.println(len);
        System.out.println(size);
java 复制代码
        InputStream is=new FileInputStream("test1021_1\\src\\a.txt");
        byte[] bytes=is.readAllBytes();//这个函数可以把文件中的所有字节都读到这个数组中,更方便
        System.out.println(new String(bytes));

其实读写文件更适合用字符流

5.2文件字节输出流

java 复制代码
        OutputStream os=new FileOutputStream("test1021_1\\src\\b.txt");//没有文件会自己创建
        os.write(97);//写字节数据
        os.write('b');
//        os.write('磊');//这样就不行了,因为这是三个字节
        byte[] bytes="我爱你中国abc".getBytes();
        os.write(bytes);
        os.write(bytes,0,15);
java 复制代码
        OutputStream os=new FileOutputStream("test1021_1\\src\\b.txt",true);//没有文件会自己创建

构造器后面加个true,就可以拼接了,原来默认是false,每次运行都会清空原来的

java 复制代码
        os.write("\r\n".getBytes());

换行符是占两个字节的,它是由\r\n组成的

文件复制,如果要复制图片的话,就要用字节流,不能用字符流

5.3 处理资源释放

我们上面写的有一个问题,就是万一在close之前就异常抛出的话,就无法关闭了

所以我们有两种处理方式

第一种就是close放在finally中

还有就是千万不要再finally中返回数据

java 复制代码
        OutputStream os= null;
        try {
            os = new FileOutputStream("test1021_1\\src\\b.txt",true);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }finally {
            os.close();
        }

还有就是close也可能抛异常,os为null的时候不能调用,所以还要继续套娃

java 复制代码
        OutputStream os= null;
        try {
            os = new FileOutputStream("test1021_1\\src\\b.txt",true);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }finally {
            try {
                if(os!=null)os.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

第二种方法就是在try的括号里面定义,就会自动close,前提在try括号里面的必须是资源

java 复制代码
        try (
                OutputStream os = new FileOutputStream("test1021_1\\src\\b.txt",true);
                ){
            //代码
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

怎么验证呢,资源通常都会实现AutoCloseable接口,我们可以自己定义资源,自己重写close,打印一下就知道了

6.字符流

和字节流差不多,只不过这个传输的是字符

java 复制代码
        try(
                Reader fr=new FileReader("test1021_1\\src\\b.txt");
                ) {
            int c;
            while ((c=fr.read())!=-1)
            {
                System.out.print((char)c);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }


java 复制代码
        try(
                Writer fw=new FileWriter("test1021_1\\src\\b.txt",true);//追加数据
                ) {
            fw.write('a');
            fw.write(97);
            fw.write('磊');
            fw.write("\r\n");
            fw.write("aaaaaaa");
            fw.write("\r\n");
            fw.write("测内存",0,1);
            char[] buffer={'黑','a','d'};
            fw.write("\r\n");
            fw.write(buffer);
            fw.write(buffer,0,1);//从下标0开始写入,写入一个字符
            
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
java 复制代码
        Writer fw=new FileWriter("test1021_1\\src\\c.txt");
        fw.write('a');
        fw.write(97);

字符流写出数据的时候,必须刷新流或者关闭流,写出去的才生效,不然一直存在缓冲区中,或者缓冲区满了,才会输出过去

java 复制代码
        Writer fw=new FileWriter("test1021_1\\src\\c.txt");//追加数据
        fw.write('a');
        fw.write(97);
        fw.flush();

而关闭流是会自动刷新的

7. 缓冲流

后面我们讲的流都是高级流了

缓冲流的作用就是在内存中开辟一个类似于缓冲区的东西,比如8kb,如果没有这个的话,就要进行16次系统调用,才可以把数据源弄过来

现在有了这个缓冲流的话,我们就可以先把数据源的8kb放在缓冲流中,相当于只用两次系统调用,因为系统调用很耗时间,所以能节约就节约,这样效率就提高了

字节缓冲输入流自带了8kb缓冲池

java 复制代码
        try(
                InputStream is=new FileInputStream("test1021_1\\src\\b.txt");
                InputStream bis=new BufferedInputStream(is);//这个就是字节缓冲输入流

                OutputStream os=new FileOutputStream("test1021_1\\src\\a.txt");
                OutputStream bos=new BufferedOutputStream(os);
                ) {
            byte[] buffer=new byte[1024];
            int len;
            while((len=bis.read(buffer))!=-1){
                bos.write(buffer,0,len);
            }

        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

这个就算你遇到一个中文截断了也没事,因为字节与字节之间可以拼接,而不像原来的打印,如果截断了,打印出来肯定也是乱码

这个就是缓冲流,相比原来,速度快多了,因为增加了缓冲流

java 复制代码
        try(
                Reader fr=new FileReader("test1021_1\\src\\b.txt");
                Reader br=new BufferedReader(fr);//这个就是字符缓冲输入流
        ) {
            char[] buffer=new char[3];
            int len;
            while((len=br.read(buffer))!=-1){
                System.out.print(new String(buffer,0,len));
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

使用特有方法

java 复制代码
        try(
                Reader fr=new FileReader("test1021_1\\src\\b.txt");
//                Reader br=new BufferedReader(fr);//这个就是字符缓冲输入流
                //因为我们要使用特有的功能,所以还是不要多态了
                BufferedReader br=new BufferedReader(fr);
        ) {
            char[] buffer=new char[3];
            String line;
            while((line=br.readLine())!=null){
                System.out.print(line);
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

但这个不会读换行符

java 复制代码
        try (
                Writer fw = new FileWriter("test1021_1\\src\\b.txt");
//                Reader br=new BufferedReader(fr);//这个就是字符缓冲输入流
                //因为我们要使用特有的功能,所以还是不要多态了
                BufferedWriter bw = new BufferedWriter(fw);
        ) {
            bw.write('a');
            bw.write("\r\n");
            bw.write('a');
            bw.newLine();
            bw.write('a');
            
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

明显缓冲流要比前面的流要快点

不同编码问题

如果代码编码和文本文件的编码不一致,使用字符流读取文本文件的时候就会出现乱码

8. 转换流

比如文本文件GBK,你写代码的地方UTF-8,字符流读取的时候就会出现乱码,转换流就可以解决这个问题

多一个参数,就是文本文件的编码形式

java 复制代码
        try(
                InputStream is=new FileInputStream("E:\\java\\test1021_1\\src\\b.txt");
                Reader isr=new InputStreamReader(is,"GBK");
                //这个就是转换流
                BufferedReader br=new BufferedReader(isr);//转换流包装成缓冲流
                ) {
            String line;
            while((line =br.readLine())!=null) {
                System.out.println(line);
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
        }


这样就可以了

如果要控制写出去的字符为GBK则这样

java 复制代码
        String data="adhiu带我";
        byte[] bytes=data.getBytes("GBK");

或者用我们这里的转化流,就是字符输出转换流

java 复制代码
        try(
                OutputStream is=new FileOutputStream("E:\\java\\test1021_1\\src\\b.txt");
                Writer isr=new OutputStreamWriter(is,"GBK");
                //这个就是转换流
                BufferedWriter br=new BufferedWriter(isr);//转换流包装成缓冲流
                ) {
            br.write("神奇的");
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

9. 打印流

java 复制代码
        try(
                PrintStream ps=new PrintStream("E:\\java\\test1021_1\\src\\a.txt");
                ) {
            ps.println(97);
            ps.println('a');
            ps.println("AdIdasS马克");
            ps.println(true);
            ps.println(99.5);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
java 复制代码
        try(
                PrintStream ps=new PrintStream("E:\\java\\test1021_1\\src\\a.txt", Charset.forName("GBK"));
                ) {
            ps.println(97);
            ps.println('a');
            ps.println("AdIdasS马克");
            ps.println(true);
            ps.println(99.5);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

还可以这样指定GBK打印过去


然后就是这个打印流,和上面的没什么区别,区别就是write方法,一个写字节,一个写字符

一般系统默认的System.out.println();是打印在控制台的,但我们可以修改

java 复制代码
        try(
                PrintStream ps=new PrintStream("E:\\java\\test1021_1\\src\\a.txt", Charset.forName("GBK"));
        ) {
            System.setOut(ps);//将System.out.println();的打印位置修改
            System.out.println("aaaaaaaaaaaaaaa");
            System.out.println("bbbbbbbbbbbbb");
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }


10.数据流


这个写进去的就是任意数据了,这个的好处就是读出来也是对应的数据

java 复制代码
        try(
                DataOutputStream ps=new DataOutputStream(new FileOutputStream("E:\\java\\test1021_1\\src\\a.txt"));
        ) {
            ps.writeInt(97);
            ps.writeDouble(97.1);
            ps.writeBoolean(true);
            ps.writeUTF("adsaf猝死");
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

这个写进去的我们不用管,因为是按照类型来写的,所以看不懂

只有写进去为字符我们才看得懂

java 复制代码
        try(
                DataInputStream ps=new DataInputStream(new FileInputStream("E:\\java\\test1021_1\\src\\a.txt"));
        ) {
            int i=ps.readInt();
            System.out.println(i);
            double d=ps.readDouble();
            System.out.println(d);
            boolean b=ps.readBoolean();
            System.out.println(b);
            String rs=ps.readUTF();
            System.out.println(rs);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

这个读取也要对应的读取,不然不对

11. 序列化流

这个流就是把java对象写入文件中--》对象序列化

把文件中的对象读出来---》反序列化


先创建一个类和对象

java 复制代码
public class User  implements Serializable {
    private  String name;

    public User(String name) {
        this.name = name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}

注意对象如果需要序列化的话,必须要继承序列化接口,就是Serializable

java 复制代码
        try(
                ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("E:\\java\\test1021_1\\src\\a.txt"));
                ) {
            User u=new User("aaaaa");
            oos.writeObject(u);//序列化对象到文件中
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

因为是以对象存储的,所以也看不懂

java 复制代码
        try(
                ObjectInputStream ois=new ObjectInputStream(new FileInputStream("E:\\java\\test1021_1\\src\\a.txt"));
                ) {
            User u=(User) ois.readObject();//这个会返回一个Object类,所以强转
            System.out.println(u);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

这个就是从文件中读取对象

java 复制代码
    private transient String name;

给name添加一个transient ,这样的话这个参数就不会参与序列化,但是原来的对象里面有name,文件中没有name,反序列化后的也没有name

这个可以对密码使用,这样就不会被看到密码了

如果要序列化多个对象,那么把这多个对象存在ArrayList集合中就可以了,因为ArrayList已经实现了序列化接口

12. 框架

框架就是jar包的格式

我们讲的是IO框架就是Commons-io

我们可以去官网下载
IO框架

下载这个然后解压就可以了

选择这个

在模块下创建文件夹,复制粘贴jar包

然后添加为库

这样就可以使用了

java 复制代码
        FileUtils.copyFile(new File(),new File());//拷贝文件
        FileUtils.copyDirectory(new File(),new File());//拷贝文件夹
        FileUtils.deleteDirectory(new File());//删除文件夹

总结

下一节对应特殊文件

相关推荐
周全全13 分钟前
Spring Boot + Vue 基于 RSA 的用户身份认证加密机制实现
java·vue.js·spring boot·安全·php
AiFlutter1 小时前
Java实现简单的搜索引擎
java·搜索引擎·mybatis
飞升不如收破烂~1 小时前
Spring boot常用注解和作用
java·spring boot·后端
秦老师Q1 小时前
Java基础第九章-Java集合框架(超详细)!!!
java·开发语言
计算机毕设源码qq-38365310411 小时前
(附项目源码)Java开发语言,215 springboot 大学生爱心互助代购网站,计算机毕设程序开发+文案(LW+PPT)
java·开发语言·spring boot·mysql·课程设计
ashane13141 小时前
Java list
java·windows·list
袁庭新1 小时前
Cannal实现MySQL主从同步环境搭建
java·数据库·mysql·计算机·java程序员·袁庭新
无尽的大道1 小时前
深入理解 Java 阻塞队列:使用场景、原理与性能优化
java·开发语言·性能优化
岁岁岁平安2 小时前
springboot实战(15)(注解@JsonFormat(pattern=“?“)、@JsonIgnore)
java·spring boot·后端·idea
Oak Zhang2 小时前
TheadLocal出现的内存泄漏具体泄漏的是什么?弱引用在里面有什么作用?什么情景什么问题?
java·系统安全