zoukankan      html  css  js  c++  java
  • Java编程思想(11~17)

    【注:此博客旨在从《Java编程思想》这本书的目录结构上来检验自己的Java基础知识,只为笔记之用】

    第十一章 持有对象

    11.1 泛型和类型安全的容器》eg: List<String> 容器中可以插入该泛型类子类都可以放置进去
    11.2 基本概念
      (1)Collection. 一个独立元素的序列,这些元素都服从一条或多条规则.List 必须按照插入的顺序保存元素,而Set不能有重复元素
        Queue按照排队规则来确定对象产生的顺序
      (2)Map. 一组成对的"键值对"对象
    11.3 添加一组元素
    11.4 容器的打印
      ArrayList和LinkedList都是List类型按照插入的顺序保存元素;ArrayList优点在于随机访问;LinkedList 优点在于插入和移除元素
      HashSet 按照hash算法插入;TreeSet按照比较的升序保存对象;LinkedHashSet 按照被添加的顺序保存对象
    11.5 List

    11.6 迭代器

    11.6.1 ListIterator》是一个更强大的Iterator的子类型,它只能用于各种List类型的访问;Iterator只能向前移动,ListIterator可以
    双向移动。
    11.7 LinkedList
    11.8 Stack
    11.9 Set 》可以使用set类型来消除重复元素
    11.10 Map
    11.11 Queue
    11.11.1 PriorityQueue
    11.12 Collection和Iterator
    11.13 Foreach与迭代器
    11.13.1 适配器方法惯用法

    第十二章 通过异常处理错误

    12.1 概念
    12.2 基本异常
    12.3 捕获异常
      12.3.1 try块
    12.3.2 异常处理程序
    12.4 创建自定义异常
      12.4.1 异常与记录日志
    12.5 异常说明》在编译时强制检查的异常被称为被检查异常
    12.6 捕获所有异常》catch exception
      12.6.1 栈轨迹
      12.6.2 重新抛出异常 》throw
      12.6.3 异常链》Throwable的子类中三种基本异常类提供了带cause参数的构造器:Error(Java虚拟机报告系统错误),Exception以及
    RuntimeException

    12.7 Java标准异常》Throwable 表示任何可以作为异常被抛出的类。Throwable对象可以分为两种类型:Error表示编译时和系统错误(除
    特殊情况外,一般不用关心);Exception 表示被抛出的异常的基本类型,在Java类库,用户方法以及运行时故障中都可能抛出Exception。Java
    开发人员关心的基本类型通常是Exception。
      12.7.1 特例:RuntimeException
    12.8 使用finally进行清理
      12.8.1 finally用来做什么
      12.8.2 在return中使用finally
      12.8.3 缺憾:异常丢失
    12.9 异常的限制
    12.10 构造器
    12.11 异常匹配
    12.12 其他可选方式
      12.12.1 历史
      12.12.2 观点
      12.12.3 把异常传送给控制台
      12.12.4 把“被检查的异常”转换为“不检查的异常”》throw new RuntimeException(e);
    12.13 异常使用指南
      (1)在恰当的级别处理问题(在知道该如何处理的情况下才捕获异常)
      (2)解决问题并且重新调用产生异常的方法
      (3)进行少许的修补,然后绕过异常发生的地方继续执行
      (4)用别的数据进行计算,以替代方法预计会返回的值
      (5)把当前运行环境下能做的事情尽量做完,然后把相同的异常重新抛到更高层
      (6)把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层
      (7)终止程序
      (8)进行简化(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦)
      (9)让类库和程序更安全

    第13章 字符串

    13.1 不可变String
    13.2 重载“+”与StringBuilder》当在Java中做字符串相加"+"时,底层会自动引入iava.lang.StringBuilder类
    StringBuilder与StringBuffer相比较,后者是线程安全的
    13.3 无意识的递归
    13.4 String上的操作
    13.5 格式化输出
      13.5.1 printf()
      13.5.2 System.out.format()
      13.5.3 Formatter类
      13.5.4 格式化说明符》其抽象语法:%[argument_index$][flags][width][.precision]conversion

      13.5.5 Formatter转换

      13.5.6 String.format()
    13.6 正则表达式
      13.6.1 基础
      13.6.2 创建正则表达式

      13.6.3 量词

      13.6.4 Pattern和Matcher
      13.6.5 split()
    13.6.6 替换操作
    13.6.7 reset()
    13.6.8 正则表达式与Java I/O》正则表达式搜索文件的内容
    13.7 扫描输入》Scanner
      13.7.1 Scanner定界符
      13.7.2 用正则表达式扫描
    13.8 StringTokenizer》可以使用Scanner或正则表达式替代

     第14章

    14.1 为什么使用RTTI》(RTTI:在运行时识别一个对象的类型)
    14.2 Class对象》Java使用Class对象来执行其RTTI,即使你正在执行的是类似转型的操作
    每个类都有一个Class对象;使用“类加载器”来生成类对象;类是在第一次使用时动态加载到JVM中的
    获取Class对象:Class.forName() ;对象.getClass(); Class对象的newInstance()创建类对象,该类必须带有默认的构造器
      14.2.1 类字面常量》获取Class对象的另一个方法:类名.class 如:String.class。使用这种方式不会自动初始化该Class对象
    为了使用类而坐的准备工作包含三步骤:
      (1)加载,这是有类加载器执行的
      (2)链接 在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必要的话,将解析这个类创建对其他类的引用
      (3)初始化 如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块
    所以,初始化操作被延迟到对静态方法(构造器隐式是静态的)或者非常态数静态进行首次引用时才执行
      14.2.2 泛化的Class引用》Class<T>
      Class<?> 优于普通的Class,尽管它们等价,但是Class<?>不会产生编译器警告信息;
      Class<?>的好处是它表示你并非是碰巧或由于疏忽,而是使用了一个非具体的类引用
      14.2.3 新的转型语法》Class对象.cast()
      Class中两个使用不常使用或没有任何用处的新特性是:Class对象.cast()接受参数对象并将其转换为Class引用的类型;
      Class.asSubclass()允许你将一个类对象转型为更加具体的类型。
    14.3 类型转换前先做检查》instanceof; class<? extends 基类>
      14.3.1 使用类字面常量》类名.class
      14.3.2 动态的instanceof》Class.isInstance方法提供了一种动态测试对象的途径
      14.3.3 递归计数
    14.4 注册工厂
    14.5 instanceof 与 Class的等价性
    14.6 反射:运行时的类信息》RTTI(Run-Time Type Identification)和反射之间真正的区别在于:对于RTTI来说,编译器在编译时
    打开和检查.class文件。而对于反射机制来说,.class文件在编译时是不可以获取的,所以在运行时开发和检查.class文件
      14.6.1 类方法提取器》Class的getMethods()和getConstructors()方法分别返回Method对象数组和Constructor对象数组
    14.7 动态代理
    14.8 空对象
      14.8.1 模拟对象与桩
    14.9 接口与类型信息》javap一个随JDK发布的反编译器
        javap -private 类名 》eg:javap -private java.lang.String
    14.10 总结反射式强大的,可以通过修改权限调用哪些被设置成private的方法

    第15章 泛型

    15.1 与C++比较

    15.2 简单泛型》Java泛型的核心概念是告诉编译器想要什么类型,然后编译器帮你处理一切细节
          泛型的主要目的是用来指定容器要持有什么类型的对象,并且有编译器来保证类型的正确性

        public class Generate<T>{
            private T t;
            Generate(T t){ this.t=t;}
        }

      15.2.1 一个元组类库
      需要在一个方法就能返回多个不同类型的对象,可是return 语句只允许返回单个对象,解决办法就是再创建一个对象,
    用它来包含想要返回的多个类型对象,当可以在每次需要的时候专门创建一个类为此工作,但是有了泛型我们就能够一次性解决
    这个问题,同时我们还能再编译器确保类型的安全。这个概念就是元组(tuple),它将多个对象存储在一个单一对象中,这个容器
    对象允许读取其中的元素,但是不允许向其中存放新的对象。元组可以任意长度,如下两个类型的元组:

        public class TwoTuple<A,B>{//这个类可以装载两个类型的
            public finale A first;
            public finale B second;
            TwoTuple(A a,B b){
                this.first=a;
                this.second=b;
            }
        }
        public class TestTuple{
            static TwoTuple<String,Integer> f(){//该方法就返回了String,Integer两种类型的对象
                return new TwoTuple<String,Integer>("hi",70);
            }
            public static void main(String[] args){
                f();
            }
        }

      15.2.2 一个堆栈类
      15.2.3 RandomList
    15.3 泛型接口

        public interface Generator<T>  {T next();}
        public class Fibonaci implements Generator<Integer>{
            private int count=0;
            public Integer next(){return fib(count++);}
            private Integer fib(int n){
                if(n<2)return 1;
                return fib(n-2)+fib(n-1);
            }
            public static void main(String[] args){
                Fibonaci f=new Fibonaci();
                for(int i=0;i<10;i++){
                    System.out.println(f.next());
                }
            }
        }

    15.4 泛型方法 》要定义泛型方法只需要将泛型参数置于返回值之前

        public class GenericMethods{
            public <T> void f(T t) {
                System.out.println(t.getClass().getSimpleName());
            }
            public static void main(String[] args) {
                GenericMethods chapterA = new GenericMethods();
                chapterA.f("");
            }
        }

      15.4.1 杠杆利用类型参数推断
      显示的类型说明:点操作符与方法名之间插入尖括号,然后把类型置于尖括号内
      如果是在定义该方法类的内部,必须在点操作符之前使用this关键字
      如果使用的static方法,必须在点操作符之前加上类名

                public class ExplicitTypeSpecification{
                    static void f(Map<Person,List<Pet>> petPeople){}
                    public static void(String[] args){
                        f(XXXX.<Person,List<Pet>>map());
                    }
                }

      15.4.2 可变参数与泛型方法

        public class GenericVarargs{
                public static <T> List<T> makeList(T... args){
                    List<T> result=new ArrayList<T> ();
                    for(T item: args){
                        result.add(item);
                    }
                    return result;
                }
                public static void main(String[] args){
                    List<String> ls=makeList("A");
                    System.out.println(ls);
                    ls=makeList("A","B","C");
                    System.out.println(ls);
                }
            }

      15.4.3 用于Generator的泛型方法
      15.4.4 一个通用的Generator
      15.4.5 简化元组的使用

     public class Tuple{
            public static <A,B> TwoTuple<A,B> tuple(A a,B b){
                return new TwoTuple<A,B>(a,b);
            }
        }

      15.4.6 一个Set使用工具

    15.5 匿名内部类

        class Teller{
            private static long counter=1;
            private final long id=counter++;
            public String toString(){return "Teller "+id;}
            public static Generator<Teller> generator=new Generator<Teller>(){....}
        }

    15.6 构建复杂模型

        public class TupleList<A,B,C,D> extends ArrayList<A,B,C,D>{
            public static void main(String[] args){
                TupleList<Vehicle,Amphibian,String,Integer> tl=new TupleList<Vehicle,Amphibian,String,Integer>();
                .....
            }                
        }

    15.7 擦除的神秘之处

        public class ErasedTypeEquivalence{
            public static void main(String[] args){
                Class c1 = new ArrayList<String>().getClass();
                Class c2 = new ArrayList<Integer>().getClass();
                System.out.println(c1 == c2);
            }    
        }

    输入的结果是:true
    在这里我们很容易的认为ArrayList<String>和ArrayList<Integer>是不同的类型,即输出的结果为false,可是Java运行
    出来的结果是true,这说明Java认为他们是同一个类型,为什么呢? 

    在看为什么钱,我们先来看看Class类中的getTypeParameters这个方法的定义和作用
    注释的意思是:返回一个TypeVariable类型的数组,TypeVarible表示泛型中使用的类型变量或者类型参数
        /**
         * Returns an array of {@code TypeVariable} objects that represent the
         * type variables declared by the generic declaration represented by this
         * {@code GenericDeclaration} object, in declaration order.  Returns an
         * array of length 0 if the underlying generic declaration declares no type
         * variables.
         *
         * @return an array of {@code TypeVariable} objects that represent
         *     the type variables declared by this generic declaration
         * @throws java.lang.reflect.GenericSignatureFormatError if the generic
         *     signature of this generic declaration does not conform to
         *     the format specified in
         *     <cite>The Java&trade; Virtual Machine Specification</cite>
         * @since 1.5
         */
        @SuppressWarnings("unchecked")
        public TypeVariable<Class<T>>[] getTypeParameters() {
            ClassRepository info = getGenericInfo();
            if (info != null)
                return (TypeVariable<Class<T>>[])info.getTypeParameters();
            else
                return (TypeVariable<Class<T>>[])new TypeVariable<?>[0];
        }

    再看如下程序:

    我们看到输入的并不是具体的类型名,而是只是无用的占位符的标识符

    因此,这说了一个现实是:在泛型代码的内部,无法获得任何有关泛型参数类型的信息  

      15.7.1 C++的方式》模板被实例化时,模板代码知道其模板参数的类型
        但是针对泛型的擦除可以用extends 定义一个上限:List<T extends Parent>,那么编译器在进行泛型类型参数
        擦除时将擦除到它的第一个边界,即就像“Parent”替换“T“一样

      15.7.2 迁移兼容性
      为了减少潜在的关于擦除的混淆,必须清楚的认识到这不是一个语言的特性.它是Java泛型实现的一种折中,因为泛型不是
    在Java语言一开始出现时就有的;如果泛型在Java 1.0中就已经有了,那么这个特性就不会使用擦除来实现了。擦除减少了泛
    型的泛化性,虽然泛型仍然很有用,只是没有我们想象的那么有用了
    在基于擦除的现实中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文环境中使用的类型。泛型只在静态类型
    检查期间才出现,在此之后,程序中所有的泛型类型都将被擦除,替换为它们的非泛型上界。例如:List<T> 将为擦除为List,
    而普通的类型变量,在未通过"extends"指定上限情况下将被擦除为Object
    擦除机制的核心动机是使采用泛化的代码可以使用费泛化的代码
      15.7.3 擦除的问题
    擦除的主要正当理由是从非泛化代码到泛化代码的转变过程,以及不破坏现有的类库的情况下,将泛型融入Java语言
    其代价就是:不能用于显示的引用运行时类型的操作中,如转型,instanceof操作和new 表达式;因为所有的参数类型信息
    都丢失了。所以,无论何时,当你在编写泛型代码时,必须时刻提醒自己,这些只是看起来好像拥有关于参数的类型信息

      15.7.4 边界处的动作
    15.8 擦除的补偿
      15.8.1 创建类型实例
      15.8.2 泛型数组》由于泛型擦除》所以一般的解决方案是在任何想要创建泛型数组的地方都是用ArrayList
    15.9 边界》 xxx<T extends SuperClass>

              或class ColoredDimension<T extends Dimension&HashColor>{}

              或class ColoredDimension<T extends Dimension & HashColor & Weight>{}

              或        

                class HoleItem<T>{}
                class Colored<T extends HasColor> extends HoleItem<T>{}


    15.10 通配符》class Demo<? extends Uper>
      15.10.1 编译器有多聪明》编译器并没有想象中的那么聪明

    public class CompilerIntellience{
        List<? extends Fruit> flist=Arrays.asList(new Apple());
        Apple a=(Apple)flist.get(0);//没有警告
        flist.contains(new Apple()); //Argument is "Object"
        flist.indexOf(new Apple()); //Argument is "Object"
    }

      15.10.2 逆变》超类通配符:super

    public class SuperTypeWildcards{
        static void wirteTo(List<? super Apple> apples){
            apples.add(new Apple());
            apples.add(new Jonathan());
            //apples.add(new Fruit()); //Error
        }
    }

      15.10.3 无界通配符》?

    public class UnboundedWildcards{
        static Map map1;
        static Map<?,?> map2;
        static Map<String,?> map3;
    }

      List和List<?>是不同的:List可以持有任何类型的组合,而List<?>将持有具有某种具体类型的同构集合

      在使用确切类型替代通配符类型的好处是可以用泛型参数来做更多的事,但是使用通配符使得你必须接受范围更宽的参数化类型作为参数

      15.10.4 捕获转换
    15.11 问题
      15.11.1 任何基本类型都不能作为类型参数

      15.11.2 实现参数化接口》一个类不同实现同一个泛型接口的两中变体,由于擦除的原因,这两个变体会成为相同的接口

      interface Payable<T>{}
      class Employee implements Payable<Employee>{}
      class Hourly extends Employee implements Payable<Horly> {} 
      //这个类不能编译,因为擦除会将Payable<Horly>和Payable<Employee>简化为相同的类Payable,这样就意味着
      //在重复两次实现相同的接口

      15.11.3 转型和警告》使用带有泛型参数的转型或instanceof不会有任何效果
      15.11.4 重载

    public class UserList<W,T>{
        void f(List<T> v){}
        void f(List<W> v){}
    }//由于擦除的原因,重载方法产生相同的方法签名,因此当擦除的参数不能产生唯一的参数列表时,必须提供明显的区别:
    public class UserList<W,T>{
        void f1(List<T> v){}
        void f2(List<W> v){}
    }

      15.11.5 基类劫持了接口
    15.12 自限定的类型
      15.12.1 古怪的循环类型
      15.12.2 自限定

      自限定所做的就是要求在继承关系中下面这样定义:
      class A extends SelfBounded<A>{}
      这回强制要求将正在定义的类当作参数传递给基类
      其意义保证类型参数必须与正在定义的类相同

      15.12.3 参数协定
    15.13 动态类型安全

      将子类的类型的对象放置到将要检查基类类型的受检查容器中是没有问题的

    15.14 异常

      由于擦除原因,泛型应用于异常是非常有限的.catch语句不能捕获泛型类型的异常,因为在编译器和运行时都必须知道异常的确切类型.
    泛型也不能直接或间接继承自Throwable.但是泛型参数可能会在一个方法的throws字句中用到.这可以让你编写随检查型异常的类型而
    发生变化的泛型代码

    interface Processor<T, E extends Exception> {}

    15.15 混型  

      混型最基本的概念是混合多个类的能力以产生一个可以表示混型中所有类型的类,其价值之一是可以将特性和行为一致的应用于多个类中.
    如果想在混型总修改某些东西,这些修改将会应用于混型所应用的所有类型之上.

      15.15.1 C++中的混型
      15.15.2 与接口混合

    interface Basic{
        private String value;
        public void set(String val);
        public String get();
    }
    interface SerivalNumbered{long getSerialNumber();}
    interface TimeStamped{long getStamp();}
    
    class BasicTmp implements Basic{
        private String value;
        public void set(String val){value=val;}
        public String get(){return value;}
    }
    
    class Mixin extends BasicTmp implements SerivalNumbered,TimeStamped{
        ....
    }

      15.15.3 使用装饰器模式
      15.15.4 与动态代理混合
    15.16 潜在类型机制》不要求静态或动态类型检查

      在Java中因为泛型是后期才添加到Java中的,因此没有任何机会可以去实现任何类型的潜在类型机制,因此Java不支持这种特性

    15.17 对缺乏潜在类型机制的补偿
      15.17.1 反射》可以使用反射来实现类似的潜在类型机制的特性
      15.17.2 将一个方法应用于序列
      15.17.3 当你并未碰巧拥有正确的接口时
      15.17.4 用适配器仿真潜在的类型机制
    15.18 将函数对象用作策略
    15.19 总结:转型真的如此之遭吗?
      15.19.1 进阶读物

    第16章
    16.1 数组为什么特殊
      在泛型和自动包装机制之前,数组与其他容器之间的区别有三方面:效率,类型和保存基本类型的能力
    在Java中数组是一种效率最高的存储和随机访问对象引用序列的方式,它是简单的线性序列,这使得访问的速度非常快速,
    但是这种快速所付出的代价就是数组对象的大小被固定了,并且在生命周期中不可改变。
    随着自动包装机制的出现,现在容器可以与数组一样方便的用于基本类型,数组唯一的优点就是效率
    16.2 数组是以第一级对象》数组标识符其实只是一个引用
    16.3 返回一个数组
    16.4 多维数组
    16.5 数组与泛型

        class ClassParameter<T>{//参数化类
            public T[] f(T[] arg){return arg;}
        }
        
        class MethodParameter{//参数化方法
            public static <T> T[] f(T[] arg){return arg;}
        }
        
        public class ParameterArrayType{
            public static void main(String[] args){
                Integer[] ints={1,2,3,4};
                Double[] doubles={1.1,2.2,3.3,4.4,5.5};
                Integer[] ints2=new ClassParameter<Integer>().f(ints);
                Double[] double2=new ClassParameter<Double>().f(doubles);
            }
        }

    Peel<Banana>[] peels=new Peel<Banana>[10];//报错
    尽管不能创建实际的持有泛型的数组对象,但是可以创建非泛型的数组
    List<String>[] ls;
    List[] la=new List[20];
    ls=(List<String>)la;//"Unchecked" warning

    一般而言,泛型在类或方法的边界处很有效,而在类或方法的内部,擦除通常会是泛型变得不适用 

    16.6 创建测试数据
      16.6.1 Arrays.fill()
      16.6.2 数据生成器
      16.6.3 从Genenrator中创建数组
    16.7 复制数组
      16.7.1 复制数组》System.arraycopy()
      16.7.2 数组的比较》Arrays.equals()
      16.7.3 数组元素比较》java.lang.Comparable接口 ; compareTo()方法
      16.7.4 数组排序》Arrays.sort()
      16.7.5 在已排序的数组中查找》Arrays.binarySearch()

    第17章 容器深入研究
    17.1 完整的容器分类法

    17.2 填充容器》Collections.nCopies() ;Collections.fill()
      17.2.1 一种Generator解决方案
      17.2.2 Map生成器
      17.2.3 使用Abstract类
    17.3 Collection的功能方法

    17.4 可选操作
      17.4.1 为获支持的操作》Arrays.asList()会生成一个基于固定大小数组的List
    注意:Arrays.asList()的结果作为构造器的参数传递给任何Collection(或者使用
    addAll()方法,或Collections.addAll()静态方法),这样可以生成允许使用所有方法的普通容器
    17.5 List的功能方法
    17.6 Set和存储顺序

    在HashSet上打星号表示,如果没有其他限制,它就是默认的选择,因为它对速度进行了优化

      17.6.1 SortedSet

    17.7 队列
      17.7.1 优先级队列
      17.7.2 双向队列
    17.8 理解Map
      17.8.1 性能

      17.8.2 SortedMap》键处于排序状态
      17.8.3 LinkedHashMap
    17.9 散列与散列码》自定义类重载hashCode()和equals()
      17.9.1 理解hashCode()
      17.9.2 为速度而散列
      17.9.3 覆盖hashCode()

    17.10 选择接口的不同实现
      17.10.1 性能测试架构
      17.10.2 对List的选择
      17.10.3 微基准测试的危险
      17.10.4 对Set的选择
      17.10.5 对Map的选择
    17.11 实用方法

      17.11.1 List的排序和查询》使用Collections中的方法
      17.11.2 设定Collection或Map为不可修改》Collections.unmodifiableCollection(c)
      17.11.3 Collections或Map的同步控制》Collections.synchronizedCollection(c)
    ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteArraySet都使用了可以避免ConcurrentModificationException的技术
    17.12 持有引用
      对象可获得则垃圾回收器不会释放它;若不可获得,那么回收就是安全
    SoftReference、WeakReference和PhantomReference由强到弱排序,对应不同级别的“可获得性”.SoftReference用以实现内存敏感
    的高速缓存。WeakReference是为实现“规范映射”而设计的,它不妨碍垃圾回收器回收映射的“键”。“规范映射”中的对象的实例可以在程
    序的多处被同时使用,以节省存储空间。PhantomReference用以调度回收器前的清理工作,它比Java终止机制更灵活。
    PhantomReference只能依赖于ReferenceQueue
      17.12.1 WeakHashMap》它被用来保存WeakReference
    17.13 Java 1.0/1.1的容器
      17.13.1 Vector和Enumeration
      17.13.2 Hashtable
      17.13.3 Stack
      17.13.4 BitSet

  • 相关阅读:
    WPF 打开文件 打开路径对话框
    WPF Button添加图片
    Delphi 正则表达式PerlRegEx
    解决Inet控件下载utf8网页乱码的问题
    Delphi程序结构
    VB 936(gb2312)URL编码与解码
    Chr 将一个有序数据转换为一个ANSI字符
    Delphi正则表达式使用方法(TPerlRegEx)
    Delphi类型转换
    Delphi 正则表达式TPerlRegEx 类的属性与方法
  • 原文地址:https://www.cnblogs.com/liaojie970/p/5581981.html
Copyright © 2011-2022 走看看