Java1.8-函數式調用

1、Map結構優化

​ java8以前HashMap存儲結構以下圖,將Map中的key進行哈希運算獲得hashCode,當出現hashCode相同但equals不一樣時稱此現象爲碰撞,發生碰撞時會造成鏈表結構,取值時會遍歷整個鏈表結構效率較低。java

hashMap-old-struct

​ java8採用的HashMap存儲結構以下圖,當發生碰撞造成的鏈表上元素個數大於8時,總容量大於64會將鏈表轉換爲紅黑樹。此種狀況下除了添加元素慢一些,其他操做(查詢,刪除)均高於鏈表結構。算法

hashMap-new-struct

​ java8以前concurrentHashMap爲HashTable的組合達到線程安全的效果,默認併發級別爲16即concurrentHashMap由16個HashTable組成。java8採用CAS算法到達線程安全的效果,數據結構爲java8的HashMap結構。數據庫

注:hashMap起始默認容器大小爲16,當容器元素個數到達75%(擴容因子)開始擴容,擴容一倍大小從新計算位置。編程

2、JVM內存結構的改變

歷史:以前不少公司生產的JVM早已沒有永久代,只是SUN的JVM尚未淘汰永久代數組

  • Oracle-SUN Hotspot
  • Oracle HRocket
  • IBM J9 JVM
  • Alibaba Taobao JVM

​ java8將永久代變爲元空間(MetaSpace)。以前永久代在JVM中分配,永久代基本不回收佔用JVM內存空間。java8廢棄永久代改成元空間,元空間在操做系統的內存上進行分配。安全

  • PremGenSize和MaxPremGenMaxSize被刪除
  • MetaSpaceSize和MetaSpcaeMaxSiez被添加

3、Lambda表達式

​ 在函數式語言中,咱們只須要給函數分配變量,並將這個函數做爲參數傳遞給其它函數就可實現特定的功能。而java如前言中所述,不能直接將方法看成一個參數傳遞。同時匿名內部類又存在諸多不便:語法過於冗餘,匿名類中的this和變量名容易令人產生誤解,類型載入和實例建立語義不夠靈活,沒法捕獲非final的局部變量等。 Lambda 表達式的出現爲 Java 添加了缺失的函數式編程特色,使咱們能將函數當作一等公民看待。數據結構

概述

​ Lambda 表達式在Java 語言中引入了一個新的語法元素和操做符。這個操做符爲-> ,該操做符被稱爲Lambda 操做符或箭頭操做符。它將Lambda 分爲兩個部分:​ 左側:指定了Lambda 表達式須要的全部參數​ 右側:指定了Lambda 體,即Lambda 表達式要執行的功能併發


​ Lambda表達式實現的必須是函數式接口。app

​ 只包含一個抽象方法的接口,稱爲函數式接口。能夠經過Lambda 表達式來建立該接口的對象。(若Lambda 表達式拋出一個受檢異常,那麼該異常須要在目標接口的抽象方法上進行聲明)。咱們能夠在任意函數式接口上使用@FunctionalInterface註解,這樣作能夠檢查它是不是一個函數式接口,同時javadoc也會包含一條聲明,說明這個接口是一個函數式接口。框架

演示

/**
 * 情景一:無參數,無返回值,一條語句
 */
@Test
public void test() throws Exception {
    Runnable r = () -> System.out.println("Hello Lambda");
    r.run();
}
/**
 * 情景二:有一個參數,而且無返回值,一條語句
 */
@Test
public void test() throws Exception {
    Consumer<String> consumer = (x) -> System.out.println("Hello " + x);
    consumer.accept("Lambda");
}
/**
* 情景三:有兩個以上參數,多條語句
*/
@Test
public void test() throws Exception {
    Comparator<Integer> comparator = (x, y) -> {
        System.out.println("多條語句");
        return Integer.compare(x, y);
    };
}

注:

  • 當參數只有一個時小括號能夠不寫,但通常狀況下不省略。
  • 當只有Lambda體中只有一條語句能夠省略{}return,通常狀況下省略。
  • Lambda表達式的參數列表的數據類型能夠省略不寫,由於JVM編譯器能夠經過上下文進行類型推斷。若是要寫須要所有寫上類型。

內置核心函數式接口

①消費型接口

@FunctionalInterface
public interface Consumer<T> {
    
    void accept(T t);
    
    //鏈式調用,以後繼續調用消費型接口
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

測試實例--消費型接口

/**
     * 消費型接口Consumer<T>
     */
    @Test
    public void test1 () {
        consumo(500, (x) -> System.out.println(x));
    }
    
    public void consumo (double money, Consumer<Double> c) {
        c.accept(money);
    }

②供給型接口

@FunctionalInterface
public interface Supplier<T> {

    T get();
}

測試實例-供給型接口

/**
     * 供給型接口,Supplier<T>
     */
    @Test
    public void test2 () {
        Random ran = new Random();
        List<Integer> list = supplier(10, () -> ran.nextInt(10));
        
        for (Integer i : list) {
            System.out.println(i);
        }
    }
    
    /**
     * 隨機產生sum個數量得集合
     * @param sum 集合內元素個數
     * @param sup
     * @return
     */
    public List<Integer> supplier(int sum, Supplier<Integer> sup){
        List<Integer> list = new ArrayList<Integer>();
        for (int i = 0; i < sum; i++) {
            list.add(sup.get());
        }
        return list;
    }

③函數型接口

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    //鏈式調用,在調用此方法以前調用傳入的函數式接口
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    //鏈式調用,在調用此方法以後調用傳入的函數式接口
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    //返回輸入的參數,Function.identity().apply("AAA")返回AAA
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

函數型接口測試實例

/**
     * 函數型接口:Function<R, T>
     */
    @Test
    public void test3 () {
        String s = strOperar(" asdf ", x -> x.substring(0, 2));
        System.out.println(s);
        String s1 = strOperar(" asdf ", x -> x.trim());
        System.out.println(s1);
    }
    
    /**
     * 字符串操做
     * @param str 須要處理得字符串
     * @param fun Function接口
     * @return 處理以後得字符傳
     */
    public String strOperar(String str, Function<String, String> fun) {
        return fun.apply(str);
    }

④斷言型接口

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    //鏈式調用,須要同時知足條件
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    //取相反值
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    //鏈式調用,只須要知足一個條件便可
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }
    
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

斷言型接口測試實例

/**
     * 斷言型接口:Predicate<T>
     */
    @Test
    public void test4 () {
        List<Integer> l = new ArrayList<>();
        l.add(102);
        l.add(172);
        l.add(13);
        l.add(82);
        l.add(109);
        List<Integer> list = filterInt(l, x -> (x > 100));
        for (Integer integer : list) {
            System.out.println(integer);
        }
    }
    
    /**
     * 過濾集合
     * @param list
     * @param pre
     * @return
     */
    public List<Integer> filterInt(List<Integer> list, Predicate<Integer> pre){
        List<Integer> l = new ArrayList<>();
        for (Integer integer : list) {
            if (pre.test(integer))
                l.add(integer);
        }
        return l;
    }

除了上述得4種類型得接口外還有其餘的一些接口供咱們使用:

  • BiFunction<T, U, R> 

   參數類型有2個,爲T,U,返回值爲R,其中方法爲R apply(T t, U u)

  • UnaryOperator<T>(Function子接口)

   參數爲T,對參數爲T的對象進行一元操做,並返回T類型結果,其中方法爲T apply(T t)

  • BinaryOperator<T>(BiFunction子接口)
    參數爲T,對參數爲T得對象進行二元操做,並返回T類型得結果,其中方法爲T apply(T t1, T t2)
  • BiConsumcr(T, U) 
    參數爲T,U無返回值,其中方法爲 void accept(T t, U u)
  • ToIntFunction<T>、ToLongFunction<T>、ToDoubleFunction<T>
    參數類型爲T,返回值分別爲int,long,double,分別計算int,long,double得函數。
  • IntFunction<R>、LongFunction<R>、DoubleFunction<R>
    參數分別爲int,long,double,返回值爲R。

4、方法引用與構造器引用

方法引用

​ 當要傳遞給Lambda體的操做,已經有實現的方法了,可使用方法引用(實現抽象方法的參數列表和返回類型,必須與方法引用方法的參數列表和返回類型保持一致)

​ 方法引用:使用操做符:: 將方法名和對象或類的名字分隔開來。以下三種主要使用狀況:

  • 對象::實例方法

    @Test
    public void test() throws Exception {
        PrintStream ps = System.out;
        Consumer<String> consumer = ps::println;
        consumer.accept("Lambda");
    }
  • 類::靜態方法

    @Test
    public void test() throws Exception {
        Comparator<Integer> comparator = Integer::compare;
    }
  • 類::實例方法,第一個參數是此方法的調用者,第二個參數是此方法的參數時可使用ClassName::MethodName

    @Test
    public void test() throws Exception {
        BiPredicate<String, String> predicate = String::equals;
      
    }

構造器引用

  • 普通對象的構造器引用
public void test() throws Exception {
    Supplier<Employee> supplier = Employee::new;//與函數式接口參數列表匹配調用對應的構造方法
    supplier.get();
}
  • 數組對象的構造器引用
@Test
public void test() throws Exception {
    //傳入的參數必須是Integer,返回是一個數組
    Function<Integer, Employee[]> function = Employee[]::new;//new Employee[num]
}

5、Stream

簡介

​ Stream 是Java8 中處理集合的關鍵抽象概念,它能夠指定你但願對集合進行的操做,能夠執行很是複雜的查找、過濾和映射數據等操做。使用Stream API 對集合數據進行操做,就相似於使用SQL 執行的數據庫查詢。也可使用Stream API 來並行執行操做。簡而言之,Stream API 提供了一種高效且易於使用的處理數據的方式。

流是數據渠道,用於操做數據源(集合、數組等)所生成的元素序列。「集合講的是數據,流講的是計算」

assets/Stream

  • Stream 本身不會存儲元素
  • Stream 不會改變源對象。相反,他們會返回一個持有結果的新Stream
  • Stream 操做是延遲執行的。這意味着他們會等到須要結果的時候才執行

Stream 的操做三個步驟

1、建立Stream

@Test
public void test() throws Exception {
    //一、經過Collection系列集合提供的stream()或parallelStream()獲取
    List<String> list = new ArrayList<String>();
    Stream<String> stream1 = list.stream();

    //二、經過Arrays中的靜態方法stream()獲取數組流
    Employee[] employees = new Employee[10];
    Stream<Employee> stream2 = Arrays.stream(employees);

    //三、經過Stream類中的靜態方法of()
    Stream<String> stream3 = Stream.of("AAA","BBB","CCC");

    //四、迭代建立無限流
    Stream<Integer> stream4 = Stream.iterate(0, (x) -> x + 1);

    //五、生成建立無限流
    Stream<Double> stream5 = Stream.generate(Math::random);
}

2、中間操做

  • 篩選

    /**
    * filter——接受Lambda,從流中排除某些元素
    * limit——截斷流,使其元素不超過給定數量
    * skip(n)——跳過元素,返回一個扔掉了前n個元素的流。若流中元素不足n個,則返回一個空流。與limit互補
*/
@Test
public void test() throws Exception {
    emps.stream().filter((e) -> e.getSalary() > 5000);
  
    emps.stream().limit(5);
  
    emps.stream().skip(5);
  
    emps.stream().distinct();
      
    //此操做會發生短路,取出工資大於5000的兩個其他再也不遍歷
    emps.stream().filter((e) -> e.getSalary() > 5000).limit(2);
}
```
  • 映射

    /**
    * map——接收Lambda,將元素轉換爲其餘形式或提取信息。接收一個函數做爲參數,該參數會被應用到每一個元素上,並將其映射成一個新元素
*/
@Test
public void test() throws Exception {
    emps.stream().map(Employee::getName);
  
    //會將【【A】,【B】,【C】】轉換爲【A,B,C】
    List<List<String>> lls = Arrays.asList(Arrays.asList("A"),Arrays.asList("B"),Arrays.asList("C"));
    lls.stream().flatMap((ls) -> ls.stream());
}
```
  • 排序

    /**
    * sorted():天然排序
*/
@Test
public void test() throws Exception {
    List<String> arr = Arrays.asList("AAA","BBB","CCC");
    arr.stream().sorted();//天然排序
    emps.stream().sorted((x, y) -> x.getAge().compareTo(y.getAge()));//定製排序
}    
```

3、終止操做(終端操做)

  • 查找與匹配

    /**
    * allMatch:檢查是否匹配全部元素
    * anyMatch:檢查是否至少匹配一個元素
    * noneMatch:檢查是否沒有匹配全部元素
    * findFirst:返回第一個元素
    * findAny:返回任意一個元素
    * count:返回元素個數
    * max:返回流中最大元素
*/
@Test
public void test() throws Exception {
    boolean allMatch = emps.stream().allMatch((e) -> e.getAge() > 18);    //employees是否年齡都大於18
    boolean anyMatch = emps.stream().anyMatch((e) -> e.getSalary() > 6000);    //employees是否存在工資大於6k
    boolean noneMatch = emps.stream().noneMatch((e) -> e.getName().equals("小明"));//employees是否沒有人叫小明
  
    Optional<Employee> findFirst = emps.stream().findFirst(); //返回第一個元素
    Optional<Employee> findAny = emps.parallelStream().findAny(); //返回任意一個元素,stream()會一直返回第一個
  
    long count = emps.stream().count(); //返回元素個數
    Optional<Employee> max = emps.stream().max((x, y) -> Integer.compare(x.getAge(),y.getAge())); //返回年齡最大的
    Optional<Employee> min = emps.stream().min((x, y) -> Integer.compare(x.getAge(),y.getAge())); //返回年齡最小的
}
```
  • 歸約與收集

    /**
    * 歸約:reduce(T indetity, BinaryOperator) / reduce(BinaryOperator):能夠將流中的元素反覆結合起來獲得一個新值
*/
@Test
public void testReduce() throws Exception {
    emps.stream().map(Employee::getSalary).reduce(0.0, Double::sum);    //計算工資總和
}
  
/**
* 收集:collect - 將流轉換爲其餘形式,接收一個Collector接口實現,用於數據彙總 Collectors:工具類用於產生Collector實例
*/
@Test
public void testCollect() throws Exception {
    List<String> names = emps.stream().map(Employee::getName).collect(Collectors.toList());
    // 轉換爲自定義數據類型
    HashSet<Double> salarys = emps.stream().map(Employee::getSalary).collect(Collectors.toCollection(HashSet::new));
    // 用收集器獲得總個數
    long count = emps.stream().collect(Collectors.counting()); 
    // 取平均值
    Double avg = emps.stream().collect(Collectors.averagingDouble(Employee::getSalary));
    // 取總和
    Double sum = emps.stream().collect(Collectors.summingDouble(Employee::getSalary));
    // 取最大值
    Optional<Employee> max = emps.stream().collect(Collectors.maxBy(
        (x, y) -> Double.compare(x.getSalary(), y.getSalary())));
    //拼接字符串
    String joinNames = emps.stream().map(Employee::getName).collect(Collectors.joining(","));
    // 經過summaryStatistics得到值
    DoubleSummaryStatistics summaryStatistics = emps.stream().collect(Collectors.summarizingDouble(Employee::getSalary));// 取總和
    summaryStatistics.getAverage();
    summaryStatistics.getCount();
    summaryStatistics.getMax();
    summaryStatistics.getMin();
    summaryStatistics.getSum();
  
    //分組,也能夠進行多級分組
    Map<Double, List<Employee>> groupSalary = emps.stream().collect(Collectors.groupingBy(Employee::getSalary)); //以工資分組
    Map<String, List<Employee>> groupAge = emps.stream().collect(Collectors.groupingBy(
        (e) -> e.getAge() > 35 ? "中年" : "青年")); //以年齡分組
  
    //分區是一種特殊的分組,結果 map至少包含兩個不一樣的分組一個true,一個false
    Map<Boolean, List<Employee>> partitionSalary = emps.stream().collect(Collectors.partitioningBy((e) -> e.getSalary() > 5000));
}
```
  • 遍歷

    @Test
    public void test() throws Exception {
        emps.stream().forEach(System.out::println);
    }

並行流與串行流

傳統線程池的缺陷

ThreadPool

Fork/Join

​ Fork/Join 框架:就是在必要的狀況下,將一個大任務,進行拆分(fork)成若干個小任務(拆到不可再拆時),再將一個個的小任務運算的結果進行join 彙總。

​ 採用工做竊取模式(work-stealing):​ 當執行新的任務時它能夠將其拆分分紅更小的任務執行,並將小任務加到線程隊列中,而後再從一個隨機線程的隊列中偷一個並把它放在本身的隊列中。相對於通常的線程池實現,fork/join框架的優點體如今對其中包含的任務的處理方式上。在通常的線程池中,若是一個線程正在執行的任務因爲某些緣由沒法繼續運行,那麼該線程會處於等待狀態。而在fork/join框架實現中,若是某個子問題因爲等待另一個子問題的完成而沒法繼續運行。那麼處理該子問題的線程會主動尋找其餘還沒有運行的子問題來執行。這種方式減小了線程的等待時間,提升了性能。

fork-join

Fork/Join的好處

fork-join-good

Fork/Join示例

public class ForkJoinCalculate extends RecursiveTask<Long> {

    private static final long serialVersionUID = 1L;
    private long start;
    private long end;
    private static final long THRESHOLD = 10000;

    public ForkJoinCalculate(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        long length = end - start;
        if(length <= THRESHOLD) {
            long sum = 0;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        }else {
            long middle = (start + end) / 2;
            ForkJoinCalculate left = new ForkJoinCalculate(start,middle);
            left.fork();
            ForkJoinCalculate right = new ForkJoinCalculate(middle + 1,end);
            right.fork();
            return left.join() + right.join();
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        ForkJoinCalculate calculate = new ForkJoinCalculate(1, 1000000000L);
        Long result = pool.invoke(calculate);
        System.out.println(result);
    }
}

並行流

​ 底層依舊使用的Fork/Join框架,使用的公共的ForkJoinPool,大大簡化了Fork/Join框架的使用難度

@Test
public void test() throws Exception {
    OptionalLong result = LongStream.rangeClosed(1, 10000000L).parallel().reduce(Long::sum);
    System.out.println(result.getAsLong());
}

6、Optional 類

​ Optional 類(java.util.Optional) 是一個容器類,表明一個值存在或不存在,原來用null 表示一個值不存在,如今Optional 能夠更好的表達這個概念。而且能夠避免空指針異常。

經常使用方法:

  • Optional.of(T t) : 建立一個Optional 實例,t爲null會拋出空指針異常
  • Optional.get() : 若是沒有值會拋出NoSuchElementException
  • Optional.empty() : 建立一個空的Optional 實例
  • Optional.ofNullable(T t):若t 不爲null,建立Optional 實例,不然建立空實例
  • isPresent() : 判斷是否包含值
  • orElse(T t) : 若是調用對象包含值,返回該值,不然返回t
  • orElseGet(Supplier s) :若是調用對象包含值,返回該值,不然返回s 獲取的值
  • map(Function f): 若是有值對其處理,並返回處理後的Optional,不然返回Optional.empty()
  • flatMap(Function mapper):與map 相似,要求返回值必須是Optional

7、接口中方法

Java 8中容許接口中包含具備具體實現的方法,該方法稱爲 默認方法,使用 ==default==關鍵字修飾。

Java 8中容許接口中定義和實現靜態方法。

接口默認方法的」類優先」原則

​ 若一個接口中定義了一個默認方法,而另一個父類或接口中又定義了一個同名的方法時選擇父類中的方法。若是一個父類提供了具體的實現,那麼接口中具備相同名稱和參數的默認方法會被忽略。

接口衝突

​ 若是一個父接口提供一個默認方法,而另外一個接口也提供了一個具備相同名稱和參數列表的方法(無論方法是不是默認方法),那麼必須覆蓋該方法來解決衝突。

interface Foo {
    default String getFoo() { // 默認方法
        return "foo";
    }
    static String showFoo() { // 靜態方法
        System.out.println("foo");    
    }
}

8、重複註解和類型註解

可重複註解

  • 在可重複註解上使用@Repeatable標註且提供容器類

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Repeatable(MyAnnotations.class)
    public @interface MyAnnotation {
      String value();
    }
  • 容器類必須提供可重複註解[] value()

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MyAnnotations {
      MyAnnotation[] value();
    }

類型註解

  • ElementType.TYPE_PARAMETER(Type parameter declaration) 用來標註類型參數

    @Target(ElementType.TYPE_PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TypeParameterAnnotation {
          
    }
      
    // 以下是該註解的使用例子
    public class TypeParameterClass<@TypeParameterAnnotation T> {
        public <@TypeParameterAnnotation U> T foo(T t) {
            return null;
        }    
    }
  • ElementType.TYPE_USE(Use of a type) 能標註任何類型名稱

    public class TestTypeUse {
      
        @Target(ElementType.TYPE_USE)
        @Retention(RetentionPolicy.RUNTIME)
        public @interface TypeUseAnnotation {
              
        }
          
        public static @TypeUseAnnotation class TypeUseClass<@TypeUseAnnotation T> extends @TypeUseAnnotation Object {
            public void foo(@TypeUseAnnotation T t) throws @TypeUseAnnotation Exception {
                  
            }
        }
          
        // 以下註解的使用都是合法的
        @SuppressWarnings({ "rawtypes", "unused", "resource" })
        public static void main(String[] args) throws Exception {
            TypeUseClass<@TypeUseAnnotation String> typeUseClass = new @TypeUseAnnotation TypeUseClass<>();
            typeUseClass.foo("");
            List<@TypeUseAnnotation Comparable> list1 = new ArrayList<>();
            List<? extends Comparable> list2 = new ArrayList<@TypeUseAnnotation Comparable>();
            @TypeUseAnnotation String text = (@TypeUseAnnotation String)new Object();
            java.util. @TypeUseAnnotation Scanner console = new java.util.@TypeUseAnnotation Scanner(System.in);
        }
    }

9、新時間與日期

​ java8提供的日期時間均是線程安全的,原始的Data、DateFormat、Calendar等均線程不安全

  • java.time.LocalDateTime 本地日期時間,包括本地時間的操做(增、減、所屬星期、月份等)
  • java.time.LocalDate 本地日期,包括本地時間的操做(增、減所屬星期、月份等)
  • java.time.LocalTime 本地時間,包括本地時間的操做(增、減所屬星期、月份等)
  • java.time.Instant 時間戳,1970.01.01:00:00:00到如今的毫秒,能夠增、減、獲得所屬星期、月份等【instant.toEpochMilli()獲得毫秒,納秒、秒等用getXXX()】
  • java.time.Duration 計算時間間隔
  • java.time.Period 計算日期間隔
  • java.time.temporal.TemporalAdjusters TemporalAdjuster【時間矯正器】的工具類,提供了下星期、明年的某天。。。
  • java.time.format.DateTimeFormatter 格式化日期時間,使用ofPattern(String)自定義格式
  • java.time.ZonedDateTime 指定時區 ,在構造時間的時候會根據時區進行時間偏移,若是在後來設置,只是更改時區並不修改時間,可以使用ZoneId得到時區
相關文章
相關標籤/搜索