Java复习之IO流(上)

作者: maoqitian | 来源:发表于2017-10-26 22:09 被阅读229次

IO流(Input Output ),即输入输出。

IO流:IO流用于处理设备间的数据传输(内存和硬盘之间)。IO流读取硬盘上的文件,对文件的操作,文件有可能不存在,操作不存在的文件,就会出现错误,Java程序在运行过程中出现的错误,而这个错误就叫做异常(Exception)。

下一篇文章传送门:
Java复习之IO流(下)

什么叫异常:

异常的概述

  • 异常就是Java程序在运行过程中出现的错误,我们来看看异常体系图。
异常继承体系.png

JVM(Java Virtual Machine)默认是如何处理异常的

  • main函数收到这个问题时,有两种处理方式:
    • 自己处理该问题,然后继续运行
    • 自己没有针对的处理方式,只有交给调用main的jvm来处理
    • jvm有一个默认的异常处理机制,对异常进行处理, 并将该异常的名称,异常的信息.异常出现的位置打印在了控制台上,同时将程序停止运行

函数自己处理异常

  • try…catch…finally
    • try后面如果跟多个catch,那么小的异常放前面,大的异常放后面,根据多态的原理,如果大的放前面,就会将所有的子类对象接收, 后面的catch就没有意义
  • throws
    • throws的方式处理异常

      • 定义功能方法时,需要把出现的问题暴露出来让调用者去处理。那么就通过throws在方法上标识。
    • throw的概述

      • 在功能方法内部出现某种情况,程序不能继续运行,需要进行跳转时,就用throw把异常对象抛出。
  • throws和throw的区别
    • throws
      • 用在方法声明后面,跟的是异常类名
      • 可以跟多个异常类名,用逗号隔开
      • 表示抛出的异常,由该方法的调用者来处理
    • throw
      • 用在方法体内,跟的是异常对象名
      • 只能抛出一个异常对象名
      • 表示抛出异常,由方法体内的语句处理
  • 下面来看个例子,如果是抛出Exception异常,则相当于是编译时异常,需要在throw抛出异常的方法中使用throws声明异常,并且在调用该方法的函数中也要做此声明,而如果是抛出运行时异常(RuntimeException),则不用throws做声明,因为他是运行时异常,运行中出现错误才会捕获。
/**
 * 
 * @author 毛麒添
 * throw 抛出异常
 * 运行时异常抛出和编译时异常抛出
 */
public class Demo_throws {
   
    public static void main(String[] args) throws Exception {
        Person p=new Person();
        p.setAge(-17);
    }
}

class Person{
     private int age;
     private String name;
     
    public int getAge() {
        return age;
    }
    public void setAge(int age) throws Exception {
        if(age>0&&age<=100){
            this.age = age;
        }else{
            throw new Exception("年龄不合法");
        }
    }
       /**
         * RuntimeException 不需要throws声明
    
    public void setAge(int age)  {
        if(age>0&&age<=100){
            this.age = age;
        }else{
            throw new RuntimeException("年龄不合法");
        }
        
    }
     */
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
     
}
运行截图.png
  • Java中的异常被分为两大类:编译时异常和运行时异常。编译期异常和运行期异常的区别:
    • 所有的RuntimeException类及其子类的实例被称为运行时异常,其他的异常就是编译时异常(运行时异常一般为我们程序猿自己犯的错误,程序运行报错,修改代码)

    • 编译时异常,Java程序必须显示处理(直接加异常处理),否则程序就会发生错误,无法通过编译(如下IO流对文件操作)

    • 运行时异常,无需显示处理,也可以和编译时异常一样处理

try {
            FileInputStream fis=new FileInputStream("aaa.txt");
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
  • finally关键字

    • finally的特点
      • 被finally控制的语句体一定会执行
      • 特殊情况:在执行到finally之前jvm退出了(比如System.exit(0))
    • finally的作用
      • 用于释放资源,在IO流操作和数据库操作中会见到
    • 顺便提一下面试中经常见到关于finally的一个面试题
      • final,finally和finalize的区别

      • final: final可以修饰类,该类不能被继承;也可以修饰方法,该方法不能被重写;还可以修饰变量,表示该变量只能赋值一次
      • finally:finally是try语句中的一个语句体,不能单独使用,用来释放资源
      • finalize:finalize是一个方法,当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
  • 自定义异常

    • 为什么要自定义异常

      • 异常名称的不同主要就是为了让我们知道到底我们程序中是有什么错误,如果都是抛出Exception,我们就很那定位程序中到底是出现了什么错误,所以如果Java中没有给我们提供针对自身程序的异常,我们就可以自定义一个(比如上面例子中的设置年龄不合法我们就可以自定义一个异常 AgeOutoflegalException)
    • 继承Exception 或者 RuntimeException等

class AgeOutoflegalException extends Exception{

    public AgeOutoflegalException() {
        super();
        // TODO Auto-generated constructor stub
    }

    public AgeOutoflegalException(String message) {
        super(message);
        // TODO Auto-generated constructor stub
    }
    
}
自定义异常运行结果.png
  • 最后来一个例子对异常做一个小结吧(异常嵌套)。
/**
 * @author 毛麒添
 *  键盘录入一个int类型的整数,对其求二进制表现形式
 *  如果录入的整数过大,给予提示,录入的整数过大请重新录入一个整数BigInteger
 *  如果录入的是小数,给予提示,录入的是小数,请重新录入一个整数
 *  如果录入的是其他字符,给予提示,录入的是非法字符,请重新录入一个整数
 */
public class Demo_Test {

    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        
        System.out.println("请输入一个整数:");
        
            while(true){
                String string = sc.nextLine();
            
                try {
                    //将字符串转换成整数
                    int num=Integer.parseInt(string);
                    System.out.println(string+"的二进制表达形式为:"+Integer.toBinaryString(num));
                    break;
                } catch (Exception e) {
                    try {
                        new BigInteger(string);//到这步说明这是一个很大的整数
                        System.out.println("录入错误,输入的是一个很大的整数,请重新输入");
                    } catch (Exception e2) {
                        try {
                            new BigDecimal(string);//到这步说明这是一个小数
                            System.out.println("录入错误,输入的是一个小数,请重新输入");
                        } catch (Exception e3) {
                            //到这一步说明输入的是非法字符
                            System.out.println("录入错误,输入的是非法字符,请重新输入");
                        }
                    }
                }
            }
    }
}

运行截图:

异常小结例子截图.png
  • File 类

前面提到,IO流操作文件,我们首先要知道文件在哪,没有这个文件则需要创建,修改文件名等操作,则我们需要用到File类。

  • File类的概述
    • File更应该叫做一个路径
      • 文件路径或者文件夹路径
      • 路径分为绝对路径和相对路径固,绝对路径是一个固定的路径,从盘符开始(例如 E:\ ,表示E盘的绝对路径); 相对路径相对于某个位置,在IDE工具下是指当前项目下,在dos操作界面下(cmd 打开控制台,C:\Users\XXXX 则表示相对路径),创建文件不写盘符则表示相对路径,写入盘符则表示指定文件位置,表示为绝对路径。
  • 构造方法

    • File(String pathname):根据一个路径得到File对象
    • File(String parent, String child):根据一个目录和一个子文件/目录得到File对象
    • File(File parent, String child):根据一个父File对象和一个子文件/目录得到File对象
    • File(URI uri):将给定的file,URI转换成一个抽象的路径名来创建一个新的 File实例
  • 创建功能

    • public boolean createNewFile():创建文件, 如果要创建的文件已经存在,就不创建了
    • public boolean mkdir():创建文件夹,如果要创建的文件夹已经存在,就不创建了
    • public boolean mkdirs():创建文件夹,如果父文件夹不存在,则自动帮你创建出来
  • 重命名和删除功能

    • public boolean renameTo(File dest):把文件重命名为指定的文件路径(如果路径名相同,就是改名,如果路径名不同,就是改名并剪切)
    • public boolean delete():删除文件或者文件夹(Java中的删除不经过回收站,要删除一个文件夹,请注意该文件夹内不能包含文件或者文件夹)
  • 判断功能

    • public boolean isDirectory():判断是否是目录
    • public boolean isFile():判断是否是文件
    • public boolean exists():判断是否存在
    • public boolean canRead():判断是否可读(Windows系统默认所有文件都是可读的,所以设置 File.setReadable(false) 无效)
    • public boolean canWrite():判断是否可写(Windows系统可以设置文件不可写,设置 File.setWritable(false) 有效)
    • public boolean isHidden():判断是否隐藏
  • 获取功能

    • public String getAbsolutePath():获取绝对路径
    • public String getPath():获取路径(获取的路径为构造方法中传入的路径,传入的是绝对路径,获取就是绝对路径;构造方法中传入相对路径就是获取相对路径)
    • public String getName():获取名称(文件或者文件夹名称)
    • public long length():获取长度。字节数
    • public long lastModified():获取最后一次的修改时间,毫秒值
    • public String[] list():获取指定目录下的所有文件或者文件夹的名称数组
    • public File[] listFiles():获取指定目录下的所有文件或者文件夹的File对象数组
  • 文件名称过滤器方法

    • public String[] list(FilenameFilter filter) 过滤文件名称
    • public File[] listFiles(FileFilter filter) 过滤文件对象
  • File 递归

    • 递归的好处:不用知道循环次数,想递归多少次由读取的数据决定

    • 递归的弊端:不能调用次数过多,对此调用容易导致栈内存溢出

    • 构造方法是否可以递归调用?

      • 构造方法不能使用递归调用
    • 递归调用是否必须有返回值?

      • 不一定(可以有,也可以没有)
/**
 * 
 * @author 毛麒添
 * 需求:判断D盘目录下是否有后缀名为.jpg的文件,如果有,就输出该文件名称 //递归
 * 需求:判断D盘目录下是否有后缀名为.jpg的文件,如果有,就输出该文件名称//文件过滤器
 */
public class Test {

    public static void main(String[] args) {
        
        demo1(new File("D:\\图片"));
         //需求:判断D盘目录下是否有后缀名为.jpg的文件,如果有,就输出该文件名称//文件过滤器
        File file=new File("D:\\图片");
        String[] list = file.list(new FilenameFilter() {
            
            @Override
            public boolean accept(File dir, String name) {
                // 将遍历的文件封装成对象
                File file2=new File(dir, name);
                return file2.isFile()&&file2.getName().endsWith(".jpg");
            }
        });
        
        for (String string : list) {
            System.out.println(string);
        }
    }

    private static void demo1(File dir) {
        //需求:判断D盘目录下是否有后缀名为.jpg的文件,如果有,就输出该文件名称 //递归 
         File[] files = dir.listFiles();
         for (File file2 : files) {
            if(file2.isFile()&& file2.getName().endsWith(".jpg")){
                System.out.println(file2);
            }else if(file2.isDirectory()){//如果是文件夹,则递归继续寻找
                demo1(file2);
            }
        }
    }

}

一不小心,扯了这么多,好吧,下面进入到本文主题 IO流

  • IO流基本概念

    • IO流用来处理设备之间的数据传输(内存和硬盘之间,相当于输入输出流在他们之间建立了一个管道,管道建立肯定要占用资源,所以我们使用输入输出流之后需要关闭流)

    • Java对数据的操作是通过流的方式,流的分类按操作类型可以分为两种:

      • 字节流:字节流可以操作任何数据类型(文本,图片,音频,视频等),在计算机中任何数据都是以字节的形式存储(一般类以 Stream结尾)
      • 字符流:字符流只能操作纯字符数据(一般类以 Reader和Writer结尾)
    • IO流常用父类:

      • 字节流抽象父类:

        • InputStream 和 OutputStream
      • 字符流抽象父类:

        • Reader 和 Writer
  • FileInputStream 和 FileOuputStream

 * 
 * @author 毛麒添
 * 读取项目中 a.txt文件中的内容,一次读取一个字节
 */

public class Demo_FileInputStream {

    public static void main(String[] args) throws IOException {
        FileInputStream fis=new FileInputStream("a.txt");//确保项目中有该文件
        int a;
        while((a=fis.read())!= -1){
            System.out.println(a);
        }
        System.out.println(a);
        fis.close();

    }

a.txt中的内容.png

运行结果:

FileInputStream例子运行截图.png
  • 从运行结果我们可以看出,1数字在机器中编码为49,2数字为50,3数字为51,而文件结束的标记为-1,看a.txt文件的属性是6字节,但是我们接收定义的值a 为什么用 int类型而不是byte?因为字节输入流可以操作任意类型的文件,比如图片音频等,这些文件底层都是以二进制形式的存储的,如果每次读取都返回byte,有可能在读到中间的时候遇到111111111,那么这11111111是byte类型的-1,我们的程序是遇到-1就会停止不读了(文件结束标志),这样文件后面的数据就读不到了,所以在读取的时候用int类型接收,如果11111111会在其前面补上24个0凑足4个字节,那么byte类型的-1就变成int类型的255了,这样可以保证整个数据读完,而结束标记的-1就是int类型,而在输出流操作文件的时候,保证文件的一致性,则会写入的时候自动将前 3个字节24位给去除。
/**
 * 
 * @author 毛麒添
 * 写入内容,一个一个字节写
 */

public class Demo_FileOutputStream {

    public static void main(String[] args) throws IOException {
        /* 如果没有改文件,则会自动创建,构造方法中,写入文件如果不加入boolean类型判断,则写入会
         * 清空之前的内容再写入,如果想继续在前面写入的基础追加,则需要boolean类型参数,true表示继续追加
         */
        FileOutputStream fos=new FileOutputStream("b.txt",true);
        //前面我们读取到123,现在则写入 机器码分别为 49 50 51
        fos.write(49); //虽然写出的是一个int数,但是在写出的时候会将前面的24个0去掉,所以写出的一个字节
        fos.write(50);
        fos.write(51);
        fos.close();
    }
}

程序每运行一次都会添加一次123

b.txt文件内容.png
  • 接下来复习一下文件的拷贝,也就是InputStream 和 OutputStream的组合使用
/**
 * 
 * @author 毛麒添
 * 文件拷贝三种方式
 * 方式一 :一个字节一个字节读取拷贝
 * 方式二:整个文件一起读取在拷贝
 * 方式三:定义小数组读取文件,在将小数组中的数据写入文件
 */

public class FileCopy {

    public static void main(String[] args) throws IOException {
        //demo1();
        //demo2();
        
        //方式三:定义小数组读取文件,在将小数组中的数据写入文件(开发中标准的文件拷贝写法)
        FileInputStream fis=new FileInputStream("a.txt");
        FileOutputStream fos=new FileOutputStream("c.txt");
        
        byte[] bytes=new byte[1024*2];
        int len;
        //这里如果不读取字节数组,则len每一次得到的是对应数据的机器码,则拷贝写入的文件大小则为机器码的字节数总和
        while((len=fis.read(bytes))!=-1){
            fos.write(bytes, 0, len);
        }
        
        fis.close();
        fos.close();
        
    }

    private static void demo2() throws FileNotFoundException, IOException {
        //方式二:整个文件一起读取在拷贝(available())
        //该方式如果读取的是大文件,当将整个大文件读取,则会导致内存溢出
                FileInputStream fis=new FileInputStream("a.txt");
                FileOutputStream fos=new FileOutputStream("c.txt");
                
                int available = fis.available();//获取文件大小
                //创建一个与该文件一样大小的字节数组
                byte[] b= new byte[available];
                //将字节数组读取到内存中
                fis.read(b);
                //将字节数组的数组写到需要拷贝到的文件中
                fos.write(b);
                
                fis.close();
                fos.close();
    }

    private static void demo1() throws FileNotFoundException, IOException {
        //方式一 :一个字节一个字节读取拷贝(该方法也是IO流的拷贝核心代码)
        FileInputStream fis=new FileInputStream("a.txt");
        FileOutputStream fos=new FileOutputStream("c.txt");
        
        int a;
        while((a=fis.read())!=-1){
            fos.write(a);
        }
        
        fis.close();
        fos.close();
    }

}

  • IO流标准异常处理写法
/**
 * 
 * @author 毛麒添
 * JDK 1.6 1.7 版本IO流标准异常处理写法
 *
 */
public class Demo_CopyFile {

    public static void main(String[] args) throws IOException {
        demo1();
        //jdk 1.7 版本
        // FileInputStream和FileOutputStream 最上层都继承了 AutoCloseable接口,自动调用close方法,所以该写法不用手动关流
        try(
                FileInputStream fis=new FileInputStream("a.txt");
                FileOutputStream fos=new FileOutputStream("b.txt");
                ){
             int a;
            
             while((a=fis.read())!=-1){
                fos.write(a);
        }
        }
    }

    private static void demo1() throws FileNotFoundException, IOException {
        // JDK1.6版本
        FileInputStream fis=null;//局部变量使用前需要赋值
        FileOutputStream fos=null;
        try {
            fis=new FileInputStream("a.txt");
            fos=new FileOutputStream("b.txt");
            
            int a;
            
            while((a=fis.read())!=-1){
                fos.write(a);
            }
        } finally{
            try{                    //try finally 嵌套保证能关一个流就尽量关闭一个
                if(fis!=null)
                    fis.close();
            }finally{

                if(fos!=null)
                fos.close();
            }
            
        }
    }

}

  • BufferedInputStream 和 BufferedOuputStream
    • BufferedInputStream 和 BufferedOuputStream相当于对FileInputStream 和 FileOuputStream进行了一次包装;举个例子:一个美女素颜出镜和画了妆出镜一般情况下应该是画了妆的美女更美,而使用BufferedInputStream 和 BufferedOuputStream则拷贝的效率则更高。
    • 缓冲思想
      • 字节流一次读写一个数组的速度明显比一次读写一个字节的速度快很多,
      • 这是加入了数组这样的缓冲区效果,java本身在设计的时候,提供了字节缓冲区流(装饰设计模式)
    • BufferedInputStream
      • BufferedInputStream内置了一个缓冲区(数组)
      • 从BufferedInputStream中读取一个字节时
      • BufferedInputStream会一次性从文件中读取8192个, 存在缓冲区中, 返回给程序一个
      • 程序再次读取时, 就不用找文件了, 直接从缓冲区中获取
      • 直到缓冲区中所有的都被使用过, 才重新从文件中读取8192个
    • BufferedOutputStream
      • BufferedOutputStream也内置了一个缓冲区(数组)
      • 程序向流中写出字节时, 不会直接写到文件, 先写到缓冲区中
      • 直到缓冲区写满, BufferedOutputStream才会把缓冲区中的数据一次性写到文件里
BufferedInputStream 和 BufferedOuputStream操作示意图.png

下面给出BufferedInputStream 和 BufferedOuputStream拷贝问价的例子

/**
 * 
 * @author 毛麒添
 * BufferedInputStream和BufferOutputStream拷贝
 */

public class Demo_BufferCopy {

    public static void main(String[] args) throws IOException {
        
        BufferedInputStream bis=new BufferedInputStream(new FileInputStream("a.txt"));
        
        BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream("c.txt"));
        
        int a;
        
        while((a=bis.read())!=-1){
            bos.write(a);
        }

        
        bis.close();
        bos.close();

    }

}
  • flush和close方法的区别
    • flush()方法
      • 用来刷新缓冲区的,刷新后可以再次写出
    • close()方法
      • 用来关闭流释放资源的的,如果是带缓冲区的流对象的close()方法,不但会关闭流,还会再关闭流之前刷新缓冲区,关闭后不能再写出
  • IO流小例子(练习)
/**
 * 
 * @author 毛麒添
 * 文件加密
 * 将写出的字节异或上一个数,这个数也就是我们的秘钥,这时候文件已经加密
 * 如果需要将文件解密,则读取加密文件再次异或我们的秘钥变可以解密
 */


public class FileEncrypt {

    public static void main(String[] args) throws IOException {
        
        String file1="a.jpg";
        
        String file2="c.jpg";
        int key=456;
        //加密
        fileEncrypt(file1, file2, key);
        
        String file3="d.jpg";
        //解密
        fileEncrypt(file2, file3, key);
    }

    public static void fileEncrypt(String file1, String file2, int key) throws FileNotFoundException, IOException {
        BufferedInputStream bis=new BufferedInputStream(new FileInputStream(file1));
        
        BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream(file2));
        
        int b;
        while((b= bis.read())!=-1){
            bos.write(b^key);//456就是我们加密的秘钥
        }
    }
}

运行结果:

图片a.png 图片C.png 图片d.png
  • 将键盘输入的字符串写入到文件当中
/**
 * 
 * @author 毛麒添
 * 将键盘输入的字符串写入到文件当中
 */
public class WriteToFile {

    public static void main(String[] args) throws IOException {
        Scanner sc=new Scanner(System.in);

        FileOutputStream fos=new FileOutputStream("write.txt");
        
        System.out.println("请在键盘中输入数据:");
        while(true){
            String nextLine = sc.nextLine();
            if("quit".equals(nextLine)){
                System.out.println("写入完成!!");
                break;
            }
            fos.write(nextLine.getBytes());//字符串写入文件必须为字符数组
            fos.write("\r\n".getBytes());//换行
        }
        
        fos.close();
    }

}

运行结果:

文件写入运行结果1.png 文件写入运行结果2.png
  • 键盘输入文件路径,将该文件拷贝到当前项目下
/**
 * 
 * @author 毛麒添
 * 键盘输入文件路径,将该文件拷贝到当前项目下
 * 
 */
public class CopyFileProgress {

    public static void main(String[] args) throws IOException {
        //键盘录入文件路径,获取该文件对象
        File file=getFile();

        BufferedInputStream bis=new BufferedInputStream(new FileInputStream(file));
        //拷贝的文件名称一样
        BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream(file.getName()));
        
        int a;
        
        while((a=bis.read())!= -1){
            bos.write(a);
        }
        System.out.println("拷贝完成");
        bis.close();
        bos.close();
        
    }

    private static File getFile() {
        Scanner sc=new Scanner(System.in);
    
        System.out.println("请输入文件的路径:");
        
        while(true){
            String nextLine = sc.nextLine();
            File file=new File(nextLine);
            if(!file.exists()){
                System.out.println("该文件不存在,请重新输入!");
            }else if(file.isDirectory()){//如果是一个路径
                System.out.println("输入的是文件路径,请重新输入!");
            }else{
                return file;
            }
        }
        
    }

}

键盘输入路径拷贝文件截图.png

本文对异常处理,File类和字节流做了复习,在下一篇文章则继续对IO流中的字符流等知识进行复习。如果大家有发现不对的地方,欢迎给我指出,大家一起学习进步。如果觉得文章对你有帮助,也请给我一个喜欢。

本系列文章:
Java复习之集合框架
Java复习之IO流(上)
Java复习之IO流(下)
Java 复习之多线程

相关文章

  • Java复习之IO流(上)

    IO流(Input Output ),即输入输出。 IO流:IO流用于处理设备间的数据传输(内存和硬盘之间)。IO...

  • Java复习之IO流(下)

    上一篇文章传送门:Java复习之IO流(上) 上一篇文章中,我们在提到 IO 流的概念时说到 IO 流分为两大类,...

  • Java之IO流详解

    title: Java之IO流详解tags: Java IO流categories: Java IO流 大多数应用...

  • #IO复习学习

    IO复习学习 IO 流 在Java IO中,流是一个核心的概念。流从概念上来说是一个连续的数据流。你既可以从流中读...

  • Java学习总结之Java IO系统(二)

    本文接着Java学习总结之Java IO系统(一),我们继续总结Java IO系统的相关知识。 字符流(Write...

  • IO流之又遇见

    今天复习了java的io流 包括 之前不太明白的 byteArrayInputStream 和byteArray...

  • 【JavaSE(十三)】JavaIO流(中)

    1 IO流 1.1 IO流概述 Java中使用 IO流 来读取和写入,读写设备上的数据、硬盘文件、内存、键盘等等,...

  • Java基础之IO流

    ##Java基础之IO流IO流常用几个类的关系如下: 字节流 字节输入流FileInputStream 读取文件用...

  • Java之IO流

    一.流的属性 书上一般说的·都是流的分类,但是我感觉说属性更合适。因为一个流只要存在,那么它就有方向,可操作的最小...

  • java之IO流

    输入流:将外设中的数据读取到内存中输出流:将内存中的数据读取到外设中 字符流的由来:其实就是字节流读取文字字节数据...

网友评论

    本文标题:Java复习之IO流(上)

    本文链接:https://www.haomeiwen.com/subject/jsjyjxtx.html