成都网站建设设计

将想法与焦点和您一起共享

Stream流整理-创新互联

一、概述

JDK 1.8新增的Stream,配合Lambda,给操作集合(Collection)提供了极大的便利。

网站建设公司,为您提供网站建设,网站制作,网页设计及定制网站建设服务,专注于成都定制网页设计,高端网页制作,对茶艺设计等多个行业拥有丰富的网站建设经验的网站建设公司。专业网站设计,网站优化推广哪家好,专业seo优化排名优化,H5建站,响应式网站。

那么什么是Stream?Stream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。

Stream可以由数组或集合创建,对流的操作分为两种:

  • 中间操作,每次返回一个新的流,可以有多个。

  • 终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。

另外,Stream有几个特性:

  1. stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。

  1. stream不会改变数据源,通常情况下会产生一个新的集合或一个值。

  1. stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。

二、常用方法 2.1 创建

Stream有三种创建方式

  1. 通过java.util.Collection.stream()方法用集合创建流

  1. 使用java.util.Arrays.stream(T[] array)方法用数组创建流

  1. 使用Stream的静态方法:of()、iterate()、generate()

2.1.1 通过集合创建流
Listlist = Arrays.asList("a", "b", "c");
// 创建一个顺序流
Streamstream = list.stream();
// 创建一个并行流
StreamparallelStream = list.parallelStream();
  • 串行流:适合存在线程安全问题、阻塞任务、重量级任务,以及需要使用同一事务的逻辑。

  • 并行流:适合没有线程安全问题、较单纯的数据处理任务。

2.1.2 使用数组创建流
int[] array = {1,3,5,6,8};
IntStreamstream= Arrays.stream(array);
2.1.3 使用静态方法
Streamstream = Stream.of(1, 2, 3, 4, 5, 6);

Streamstream2 = Stream.iterate(0, (x) ->x + 3).limit(4);
stream2.forEach(System.out::println);

Streamstream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(System.out::println);

输出结果:

0 3 6 9
0.6796156909271994
0.1914314208854283
0.8116932592396652

stream和parallelStream的简单区分:stream是顺序流,由主线程按顺序对流执行操作,而parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。例如:筛选集合中的奇数,两者的处理不同之处:如果流中的数据量足够大,并行流可以加快处速度。

除了直接创建并行流,还可以通过parallel()把顺序流转换成并行流:

OptionalfindFirst = list.stream().parallel().filter(x ->x >6).findFirst();

关于更多的Parallel Stream,请参考:Java Parallel Stream和Java Stream之Parallel Streams编程指南

2.2 中间操作 2.2.1 筛选与切片
  1. filter(Predicate):筛选流中某些元素

  1. limit(long val):截断流,取流中前val个元素

  1. skip(n):跳过n元素,配合limit(n)可实现分页

  1. distinct():通过流所生成元素的equals和hashCode去重

2.2.2 映射
  1. map(Function f):接收流中元素,并且将其映射成为新元素,例如:从student对象中取name属性

  1. flatMap(Function f):将所有流中的元素并到一起连接成一个流

2.2.3 消费
  1. peek(Consumer c):获取流中元素,操作流中元素,与foreach不同的是不会截断流,可继续操作。

使用场景:当遍历完数组后还有后续操作时或list数组转stream时,不适合在用Iterable的foreach循环,这个时候peek就派上用场了。
2.2.4 排序
  1. sorted()/sorted(Comparator):产生一个新流,按照自然顺序/比较器规则排序

ArrayListnumber = new ArrayList<>();
        number.add(12);
        number.add(121);
        number.add(23);
        number.add(45);
        number.add(67);
        number.add(67);
        number.add(77);
        number.add(98);
        number.add(99);
        number.add(67);
        Streamdistinct = number.stream().filter(x ->x >50)//121,67,67,77,98,99,67
                .skip(2)//67,77,98,99,67
                .sorted()//67,67,77,98,99
                .distinct()//67,77,98,99
                .limit(3);//67,77,98
        distinct.forEach(System.out::println);
2.3 终止操作 2.3.1 匹配/聚合操作

匹配


  1. allMatch(Predicate):当流中每个元素都符合该断言时才返回true,否则返回false

  1. noneMatch(Predicate):当流中每个元素都不符合该断言时才返回true,否则返回false

  1. anyMatch(Predicate):只要流中有一个元素满足该断言则返回true,否则返回false

寻找元素


  1. findFirst():返回流中第一个元素

  1. findAny():返回流中的任意元素

计数和极值


  1. count():返回流中元素的总个数

  1. max():返回流中元素大值

  1. min():返回流中元素最小值

2.3.2 归约操作
  1. Optional reduce(BinaryOperator accumulator):第一次执行时,accumulator函数的第一个参数为流中的第一个元素,第二个参数为流中元素的第二个元素;第二次执行时,第一个参数为第一次函数执行的结果,第二个参数为流中的第三个元素;依次类推。

  1. T reduce(T identity, BinaryOperator accumulator):流程跟上面一样,只是第一次执行时,accumulator函数的第一个参数为identity,而第二个参数为流中的第一个元素。

  1. U reduce(U identity, BiFunctionaccumulator, BinaryOperator combiner):在串行流(stream)中,该方法跟第二个方法一样,即第三个参数combiner不会起作用。在并行流(parallelStream)中,我们知道流被fork join出多个线程进行执行,此时每个线程的执行流程就跟第二个方法reduce(identity, accumulator)一样,而第三个参数combiner函数,则是将每个线程的执行结果当成一个新的流,然后使用第一个方法reduce(accumulator)流程进行归约。

2.3.3 收集操作

collect:接收一个Collector实例,将流中元素收集成另外一个数据结构。

Collector是一个接口,有以下5个抽象方法:

  1. Supplier supplier():创建一个结果容器A

  1. BiConsumer accumulator():消费型接口,第一个参数为容器A,第二个参数为流中元素T

  1. BinaryOperator combiner():函数接口,该参数的作用跟上一个方法(reduce)中的combiner参数一样,将并行流中各个子进程的运行结果(accumulator函数操作后的容器A)进行合并。

  1. Function finisher():函数式接口,参数为:容器A,返回类型为:collect方法最终想要的结果R。

Set characteristics():返回一个不可变的Set集合,用来表明该Collector的特征。有以下三个特征:

  • CONCURRENT:表示此收集器支持并发。(官方文档还有其他描述,暂时没去探索,故不作过多翻译)

  • UNORDERED:表示该收集操作不会保留流中元素原有的顺序。

  • IDENTITY_FINISH:表示finisher参数只是标识而已,可忽略。

collect主要依赖java.util.stream.Collectors类内置的静态方法。Collectors具体方法如下:

归集

  1. toList():将元素收集到一个新的List。

  1. toMap():将元素收集到Map中,Map其键和值是将提供的映射函数应用于元素的结果。

  1. toSet():将元素收集到一个新的Set。

  1. toCollection():将元素Collection按遇到顺序收集到一个new中。

  1. toConcurrentMap():将元素收集到ConcurrentMap的并发对象,其键和值是将提供的映射函数应用于元素的结果。

  1. toUnmodifiableList():将元素收集到一个不可修改的List集合中。任何修改List集合的操作都将导致UnsupportedOperationException。

  1. toUnmodifiableSet():将元素收集到一个不可修改的Set集合中。任何修改Set集合的操作都将导致UnsupportedOperationException。


统计

  1. counting():返回计算元素数量,如果没有元素,则结果为0。

  1. averagingDouble():应用于元素的double值函数的算术平均值。如果没有元素,则结果为0。

  1. averagingInt():应用于元素的int值函数的算术平均值。

  1. averagingLong():应用于元素的long值函数的算术平均值。

  1. summarizingDouble():将生成double映射函数应用于每个元素,并返回结果值的汇总统计信息。

  1. summarizingInt():将生成int映射函数应用于每个元素,并返回结果值的汇总统计信息。

  1. summarizingLong():将生成long映射函数应用于每个元素,并返回结果值的汇总统计信息。

  1. summingDouble():应用于元素的double值函数的总和。

  1. summingInt():应用于元素的int值函数的总和。

  1. summingLong():应用于元素的long值函数的总和。

  1. maxBy():根据给定的比较器产生大元素。

  1. minBy():根据给定的比较器产生最小元素。


分组

  1. groupingBy():根据分类函数对元素进行分组,并返回结果Map。

  1. groupingByConcurrent():并发执行,根据分类函数对元素进行分组。

  1. partitioningBy():对元素进行分区Predicate,并将它们组织成Map>。


接合

  1. joining():按遇到顺序将元素连接成String。


归约

  1. reducing():在指定的BinaryOperator下执行其元素的缩减。


映射

  1. mapping():通过在累加之前对每个元素应用映射函数。


结果集处理

  1. collectingAndThen():调整Collector执行其它的结束转换。

三、使用案例

在使用Stream之前,先理解一个概念:Optional,Optional类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。

案例使用的员工类,这是后面案例中使用的员工类:

//学生类
package StreamCain;
public class Student {
    private String name;
    private String sex;
    private int age;
    private double soux;

    //无参数
    public Student(){}
    //有参数构造方法
    public Student(String name,String sex,int age,double soux){
        this.name=name;
        this.sex=sex;
        this.age=age;
        this.soux=soux;
    }
    public String getName() {
        return name;
    }

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

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public double getSoux() {
        return soux;
    }

    public void setSoux(double soux) {
        this.soux = soux;
    }
}

//老师类

package StreamCain;

import java.util.ArrayList;
import java.util.IntSummaryStatistics;
import java.util.stream.Collectors;

public class Teacher {
    public static void main(String[] args) {
        Student cainfly = new Student("Cainfly", "男", 25, 198);
        Student ss = new Student("ss", "女", 25, 176);
        Student mm = new Student("mm", "男", 56, 156);
        Student gg = new Student("gg", "女", 34, 136);
        Student ff = new Student("ff", "女", 12, 98);
        Student hh = new Student("hh", "男", 4, 77);
        //将这些对象添加到集合中
        ArrayListstudent = new ArrayList<>();
        student.add(cainfly);
        student.add(ss);
        student.add(mm);
        student.add(gg);
        student.add(ff);
        student.add(hh);
        System.out.println(student);
        Integer sunage = student.stream().collect(Collectors.summingInt(Student::getAge));
        //::方法的引用
        System.out.println(sunage);
    }
}
3.1 遍历/匹配(foreach/find/match)

Stream也是支持类似集合的遍历和匹配元素的,只是Stream中的元素是以Optional类型存在的。Stream的遍历、匹配非常简单。

publicclassStreamTest {
    publicstaticvoidmain(String[] args) {
        Listlist = Arrays.asList(7, 6, 9, 3, 8, 2, 1);
        // 匹配第一个
        OptionalfindFirst = list.stream().filter(x ->x >6).findFirst();
        // 匹配任意(适用于并行流)
        OptionalfindAny = list.parallelStream().filter(x ->x >6).findAny();
        // 是否包含符合特定条件的元素booleananyMatch= list.stream().anyMatch(x ->x >6);
        System.out.println("匹配第一个值:" + findFirst.get());
        System.out.println("匹配任意一个值:" + findAny.get());
        System.out.println("是否存在大于6的值:" + anyMatch);
    }
}

运行结果:

匹配第一个值:7
匹配任意一个值:8
是否存在大于6的值:true

list.forEach()与list.stream().forEach()区别

list.forEach()使用增强for循环。list.stream().forEach()首先将集合转换为流,然后对集合的流进行迭代。最后调用ReferencePipeline类的forEach方法。

publicvoidforEach(Consumeraction) {
     if (!isParallel()) {
         sourceStageSpliterator().forEachRemaining(action);
     } else {
         super.forEach(action);
     }
 }

forEachRemaining方法对集合中剩余的元素进行操作,也就是说只遍历一次集合元素。

  1. 当一边遍历一边删除的时候,forEach能够快速失败,而stream().forEach()只有等到数组遍历完之后才会抛异常。

3.2 筛选(filter)

筛选,是按照一定的规则校验流中的元素,将符合条件的元素提取到新的流中的操作。

案例一:筛选出Integer集合中大于7的元素,并打印出来

publicclassStreamTest {
    publicstaticvoidmain(String[] args) {
        Listlist = Arrays.asList(6, 7, 3, 8, 1, 2, 9);
        Streamstream = list.stream();
        stream.filter(x ->x >7).forEach(System.out::println);
    }
}

预期结果:

8 9
3.3 聚合(max/min/count)

max、min、count这些字眼你一定不陌生,没错,在mysql中我们常用它们进行数据统计。Java stream中也引入了这些概念和用法,极大地方便了我们对集合、数组的数据统计工作。

案例一:获取String集合中最长的元素。

publicclassStreamTest {
    publicstaticvoidmain(String[] args) {
        Listlist = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd");
        // 比较
        Optionalmax = list.stream().max(Comparator.comparing(String::length));
        System.out.println("最长的字符串:" + max.get());
    }
}

输出结果:

最长的字符串:weoujgsd

案例二:获取Integer集合中的大值。

publicclassStreamTest {
    publicstaticvoidmain(String[] args) {
        Listlist = Arrays.asList(7, 6, 9, 4, 11, 6);
        // 自然排序
        Optionalmax = list.stream().max(Integer::compareTo);
        // 自定义排序
        Optionalmax2 = list.stream().max(newComparator() {
            @Overridepublicintcompare(Integer o1, Integer o2) {
                return o1.compareTo(o2);
            }
        });
        System.out.println("自然排序的大值:" + max.get());
        System.out.println("自定义排序的大值:" + max2.get());
    }
}

输出结果:

自然排序的大值:11
自定义排序的大值:11

案例三:获取学生年龄最高的人。

Optionalmax = student.stream().max(Comparator.comparingInt(Student::getAge));
        System.out.println(max.get().getAge());//56

案例四:计算Integer集合中大于6的元素的个数。

publicclassStreamTest {
    publicstaticvoidmain(String[] args) {
        Listlist = Arrays.asList(7, 6, 4, 8, 2, 11, 9);
        longcount= list.stream().filter(x ->x >6).count();
        System.out.println("list中大于6的元素个数:" + count);
    }
}

输出结果:

list中大于6的元素个数:4
3.4 映射(map/flatMap)

映射,可以将一个流的元素按照一定的映射规则映射到另一个流中。分为map和flatMap:

  • map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。

  • flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。

案例一:英文字符串数组的元素全部改为大写。整数数组每个元素加3。

publicclassStreamTest {
    publicstaticvoidmain(String[] args) {
        String[] strArr = {"abcd", "bcdd", "defde", "fTr"};
        // 数组元素转大写
        ListstrList = Arrays.stream(strArr).map(String::toUpperCase)
                .collect(Collectors.toList());

        ListintList = Arrays.asList(1, 3, 5, 7, 9, 11);
        // 元素加法
        ListintListNew = intList.stream().map(x ->x + 3).collect(Collectors.toList());

        System.out.println("每个元素大写:" + strList);
        System.out.println("每个元素 + 3:" + intListNew);
    }
}

输出结果:

每个元素大写:[ABCD, BCDD, DEFDE, FTR]
每个元素 + 3:[4, 6, 8, 10, 12, 14]

案例二:将员工的薪资全部增加1000。

publicclassStreamTest {
    publicstaticvoidmain(String[] args) {
        ListemployeeList = init();

        // 不改变原来员工集合的方式
        ListemployeeListNew = employeeList.stream().map(employee ->{
            EmployeeemployeeNew=newEmployee(employee.getName(), 0, 0, null, null);
            employeeNew.setSalary(employee.getSalary() + 10000);
            return employeeNew;
        }).collect(Collectors.toList());
        System.out.println("一次改动前:" + employeeList.get(0).getName() 
                                        + "-->" + employeeList.get(0).getSalary());
        System.out.println("一次改动后:" + employeeListNew.get(0).getName()
                                        + "-->" + employeeListNew.get(0).getSalary());

        // 改变原来员工集合的方式
        ListemployeeListNew2 = employeeList.stream().map(employee ->{
            employee.setSalary(employee.getSalary() + 10000);
            return employee;
        }).collect(Collectors.toList());
        System.out.println("二次改动前:" + employeeList.get(0).getName()
                                        + "-->" + employeeListNew.get(0).getSalary());
        System.out.println("二次改动后:" + employeeListNew2.get(0).getName()
                                        + "-->" + employeeListNew.get(0).getSalary());
    }
}

输出结果:

一次改动前:Tom–>8900
一次改动后:Tom–>18900
二次改动前:Tom–>18900
二次改动后:Tom–>18900

案例三:将两个字符数组合并成一个新的字符数组。

publicclassStreamTest {
    publicstaticvoidmain(String[] args) {
        Listlist = Arrays.asList("m, k, l, a", "1, 3, 5, 7");
        ListlistNew = list.stream().flatMap(s ->{
            // 将每个元素转换成一个stream
            String[] split = s.split(",");
            Streams2 = Arrays.stream(split);
            return s2;
        }).collect(Collectors.toList());
        
        System.out.println("处理前的集合:" + list);
        System.out.println("处理后的集合:" + listNew);
    }
}

输出结果:

处理前的集合:[m-k-l-a, 1-3-5-7]
处理后的集合:[m, k, l, a, 1, 3, 5, 7]
3.5 归约(reduce)

归约,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。

案例一:求Integer集合的元素之和、乘积和大值。

publicclassStreamTest {
    publicstaticvoidmain(String[] args) {
        Listlist = Arrays.asList(1, 3, 2, 8, 11, 4);
        // 求和方式1
        Optionalsum = list.stream().reduce((x, y) ->x + y);
        // 求和方式2
        Optionalsum2 = list.stream().reduce(Integer::sum);
        // 求和方式3Integersum3= list.stream().reduce(0, Integer::sum);
        
        // 求乘积
        Optionalproduct = list.stream().reduce((x, y) ->x * y);

        // 求大值方式1
        Optionalmax = list.stream().reduce((x, y) ->x >y ? x : y);
        // 求大值写法2Integermax2= list.stream().reduce(1, Integer::max);
	    
        System.out.println("list求和:" + sum.get() + "," + sum2.get() + "," + sum3);
        System.out.println("list求积:" + product.get());
        System.out.println("list求和:" + max.get() + "," + max2);
    }
}

输出结果:

list求和:29,29,29
list求积:2112
list求和:11,11

案例二:求所有学生的工资之和

Double reduce1 = student.stream().map(Student::getSoux).reduce(0.0, Double::sum);
        Optionalreduce = student.stream().map(Student::getSoux).reduce(Double::sum);
        System.out.println(reduce1);//841.0
        System.out.println(reduce.get().doubleValue());//841.0
3.6 收集(collect)

collect(收集),可以说是内容最繁多、功能最丰富的部分了。从字面上去理解,就是把一个流收集起来,最终可以是收集成一个值也可以收集成一个新的集合。collect主要依赖java.util.stream.Collectors类内置的静态方法。

3.6.1 归集(toList/toSet/toMap)

因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toList、toSet和toMap比较常用,另外还有toCollection、toConcurrentMap等复杂一些的用法。

下面用一个案例演示toList、toSet和toMap:

ArrayListobjects = new ArrayList<>();
        objects.add("i");
        objects.add("123");
        objects.add("aa");
        objects.add("vv");
        objects.add("vv");
        objects.add("you");
        Listlist = objects.stream().collect(Collectors.toList());//转list
        Setset = objects.stream().collect(Collectors.toSet());//转set
        String joining = objects.stream().collect(Collectors.joining(","));//拼接
        ArrayListnewList= objects.stream().collect(Collectors.toCollection(ArrayList::new));//不是转为了list或set,而是转为了指定的集合
        System.out.println("list的结果为:"+list);
        System.out.println("set的结果为:"+set);//唯一性
        System.out.println("joining的结果为:"+joining);
        System.out.println(newList);

运行结果:

list的结果为:[i, 123, aa, vv, vv, you]
set的结果为:[aa, vv, 123, i, you]
joining的结果为:i,123,aa,vv,vv,you
[i, 123, aa, vv, vv, you]
3.6.2 统计(count/averaging)

Collectors提供了一系列用于数据统计的静态方法:

  1. 计数:count

  1. 平均值:averagingInt、averagingLong、averagingDouble

  1. 最值:maxBy、minBy

  1. 求和:summingInt、summingLong、summingDouble

  1. 统计以上所有:summarizingInt、summarizingLong、summarizingDouble

案例:统计学生人数、平均工资、年龄总合、最高工资。

   Long count = student.stream().collect(Collectors.counting());//总数
    Double aveng = student.stream().collect(Collectors.averagingDouble(Student::getSoux));//平均数
    Integer age = student.stream().collect(Collectors.summingInt(Student::getAge));//年龄总和
    Optionalmaxage = student.stream().map(Student::getAge).collect((Collectors.maxBy(Integer::compare)));//年龄的大值
    Optionalmax = student.stream().max(Comparator.comparingInt(Student::getAge));//年龄的大值
        System.out.println(count);//6
        System.out.println(aveng);//140.16666666666666
        System.out.println(age);//156
        System.out.println(maxage.get().toString());//56
        System.out.println(max.get().getAge());//56
3.6.3 接合(joining)

joining可以将stream中的元素用特定的连接符(没有的话,则直接连接)连接成一个字符串。

   String name = student.stream().map(Student::getName).collect(Collectors.joining(","));
    System.out.println(name);//Cainfly,ss,mm,gg,ff,hh
    	Listlist = Arrays.asList("A", "B", "C");
    	Stringstring= list.stream().collect(Collectors.joining("-"));
    	System.out.println("拼接后的字符串:" + string);//A-B-C
    }
}
3.6.4 归约(reducing)

Collectors类提供的reducing方法,相比于stream本身的reduce方法,增加了对自定义归约的支持。

   Double reduce1 = student.stream().map(Student::getSoux).reduce(0.0, Double::sum);
     Optionalreduce = student.stream().map(Student::getSoux).reduce(Double::sum);
        System.out.println(reduce1);//841.0
        System.out.println(reduce.get().doubleValue());//841.0
3.7 排序(sorted)

sorted,中间操作。有两种排序:

sorted():自然排序,流中元素需实现Comparable接口

sorted(Comparator com):Comparator排序器自定义排序

案例:将员工按工资由高到低(工资一样则按年龄由大到小)排序

// 按工资升序排序(自然排序)
        Listcollect = student.stream().sorted(Comparator.comparing(Student::getSoux)).map(Student::getName).collect(Collectors.toList());
        System.out.println(collect);
四、拓展 4.1 peek与map的区别
Streampeek(Consumeraction)Streammap(Functionmapper);

peek接收一个Consumer,而map接收一个Function。

  • map:用于对流中的每个元素进行映射处理,然后再形成新的流;

  • peek:用于debug调试流中间结果,不能形成新的流,但能修改引用类型字段的值;

Consumer是没有返回值的,它只是对Stream中的元素进行某些操作,但是操作之后的数据并不返回到Stream中,所以Stream中的元素还是原来的元素。

而Function是有返回值的,这意味着对于Stream的元素的所有操作都会作为新的结果返回到Stream中。

这就是为什么peek String不会发生变化而peek Object会发送变化的原因。

4.2 peek和foreach区别
  • peek:会继续返回Stream对象

  • forEach:返回void,结束Stream操作。

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


本文名称:Stream流整理-创新互联
网页地址:http://chengdu.cdxwcx.cn/article/ejgid.html