zoukankan      html  css  js  c++  java
  • Java基础知识总结

    Go看完了就来看看java叭        (其实5105结课之后就该总结一下的,太懒了

    大部分整理自 https://github.com/CyC2018/CS-Notes ,加上了一些自己过去的笔记


    1.数据类型

    1.1 基础数据类型/包装数据类型

    有人可能会问:int和Integer啥区别呀?它们分别属于基础/包装数据类型。区别就是Integer是对象,int只是基本数据类型。

    • byte/8
    • char/16
    • short/16
    • int/32
    • float/32
    • long/64
    • double/64
    • boolean/1
    int d=5;
    
    Integer d=new Integer(5);
    Integer d=500;    //指向java常量池中的对象

    基本数据类型的变量在栈。对象的变量在堆,栈中只有引用。

    Object类:所有类的直接或间接父类   常用方法:equals() getClass() toString() finalize()

    •   Object creatNewInstanceOf (object obj) { return obj.getClass().newInstance(); }

    基本数据类型的包装类:Character,Byte,Short,Integer,Long,Float,Double,Boolean

    •   valueOf(String) toString() 与字符串相互转换
    •   intValue()得到包装的值
    •   Math:Math.abs()     Math.max()    Math.pow()    Math.sqrt()

    包装数据类型有缓存池的概念。例如new Integer(123) 与 Integer.valueOf(123) 的区别在于:

    • new Integer(123) 每次都会新建一个对象;
    • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
    Integer x = new Integer(123);
    Integer y = new Integer(123);
    System.out.println(x == y);    // false
    Integer z = Integer.valueOf(123);
    Integer k = Integer.valueOf(123);
    System.out.println(z == k);   // true

    装箱/拆箱

    JDK1.5引入了自动装箱与自动拆箱功能,Java可根据上下文,实现int/Integer,  double/Double,  boolean/Boolean等基本类型与相应对象之间的自动转换,为开发过程带来极大便利。最常用的是通过new方法构建Integer对象。

    Integer x = 2;     // 装箱        Integer x=Integer.valueOf(10);
    int y = x; // 拆箱 int y=x.intValue();

    因为大部分数据操作都是集中在有限的、较小的数值范围,在JDK1.5 中新增了静态工厂方法 valueOf,其背后实现是将int值为-128 到 127 之间的Integer对象进行缓存,在调用时候直接从缓存中获取,进而提升构建对象的性能,也就是说使用该方法后,如果两个对象的int值相同且落在缓存值范围内,那么这个两个对象就是同一个对象;valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。

    编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象

    Integer m = 123;
    Integer n = 123;
    System.out.println(m == n); // true
    • 当值较小且频繁使用时,推荐优先使用整型池方法(时间与空间性能俱佳)。
    • 基本类型均具有取值范围,在大数*大数的时候,有可能会出现越界的情况。
    • 优先使用基本类型。原则上,建议避免无意中的装箱、拆箱行为,尤其是在性能敏感的场合,
    • 如果有线程安全的计算需要,建议考虑使用类型AtomicInteger、AtomicLong 这样的线程安全类。部分比较宽的基本数据类型,比如 float、double,甚至不能保证更新操作的原子性,可能出现程序读取到只更新了一半数据位的数值。

    1.2 数组和枚举类型

    enum Light {Red, Yellow, Green};
    
    Light ll=Light.Red;
    
    if(ll==Red){
        ...
    }

    枚举可以看做一种特殊类,里面可以定义变量和方法。

    int[] a=new int[5];
    
    for(int idx:a)
        System.out.println(idx);
    
    int[] src={0,1,2,3,4,5};
    
    int[][] a={{1,2}, {3,4,5}, {3}};
    
    int[][] t=new int[3][];
    t[0]=new int[2];
    t[1]=new int[5];
    //t[0], t[1], t[2]长度可以不同

    1.3 String

    String 被声明为 final,因此它不可被继承。在 Java 8 中,String 内部使用 char 数组存储数据。value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
    }

    为什么string要这么设计呢?

    • 1. 可以缓存 hash 值。因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
    • 2. String Pool(后面会提到) 的需要。如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
    • 3. 安全性。String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
    • 4. 线程安全。String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

    String的常用方法

    • String类的下述方法能创建并返回一个新的String对象实例(并不是原来的String变了): concat, replace, replaceAll, substring, toLowerCase, toUpperCase, trim, toString.
    • 查找: endsWith, startsWith, indexOf, lastIndexOf.
    • 比较: equals, equalsIgnoreCase
    • 字符及长度: charAt(i),length
    • 附加:s.append(a)        //相当于s=s+a
    • StringBuffer类对象保存可修改的Unicode字符序列:append, insert, reverse, setCharAt, setLength

    如果需要改变咋办?用StringBuffer

    • String 不可变
    • StringBuffer 和 StringBuilder 可变
    • String 不可变,因此是线程安全的
    • StringBuilder 不是线程安全的
    • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

    String Pool

    字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。

    当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

    有时候我们会使用  new String("abc")  。使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。

    • "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量;
    • 而使用 new 的方式会在堆中创建一个字符串对象。

    下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串

    String s1 = new String("aaa");
    String s2 = new String("aaa");
    System.out.println(s1 == s2);           // false
    String s3 = s1.intern();
    String s4 = s1.intern();
    System.out.println(s3 == s4);           // true

    如果是采用 "bbb" 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。

    String s5 = "bbb";
    String s6 = "bbb";
    System.out.println(s5 == s6);  // true

    在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

    1.4 运算符

    等号== 和 equals

    Integer i = new Integer(10);
    Integer j = new Integer(10);
    println(i==j);
    //false,因为对象是两个
    
    Integer m = 10;
    Integer n = 10;
    println(m==n);
    //true,因为对象有缓存
    
    Integer p = 200;
    Integer q = 200;
    println(p==q);
    //false,因为对象是两个

    If the value p being boxed is true, false, a byte, or a char in the range u0000 to u007f, or an int or short number between -128 and 127 (inclusive), then let r1 and r2 be the results of any two boxing conversions of p. It is always the case that r1 == r2. (生成好的对象m,n的值被缓存了,参考前面的缓存池)

    基本类型/枚举类型:内部进行了惟一实例化,所以== 判断两个值是否相等,基本类型没有 equals() 方法。

    引用对象:==是直接看两个变量是否引用同一个对象。如果要判断内容是否一样,则要重写equals方法(eg: 判断String相等)。而如果重写equals方法,则最好重写 hashCode()方法

    String比较:相同的字符串常量直接用==比较

    String hello = "Hello", lo = "lo";             //都是常量
    println( hello == "Hello");                    //true
    println( Other.hello == hello );               //true
    println( hello == ("Hel"+"lo") );              //true
    println( hello == ("Hel"+lo));                 //false表达式变量
    println( hello == new String("Hello"));        //false
    println( hello == ("Hel"+lo).intern());        //true  intern()返回内容

    Integer x = new Integer(1);
    Integer y = new Integer(1);
    System.out.println(x.equals(y)); // true
    System.out.println(x == y);      // false

    equals()的一些性质

    Ⅰ 自反性
    x.equals(x); // true
    
    Ⅱ 对称性
    x.equals(y) == y.equals(x); // true
    
    Ⅲ 传递性
    if (x.equals(y) && y.equals(z))
        x.equals(z); // true;
    
    Ⅳ 一致性
    多次调用 equals() 方法结果不变
    x.equals(y) == x.equals(y); // true
    
    Ⅴ 与 null 的比较
    对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
    x.equals(null); // false;

    如何实现一个equals()呢?

    • 检查是否为同一个对象的引用,如果是直接返回 true;
    • 检查是否是同一个类型,如果不是,直接返回 false;
    • 将 Object 对象进行转型;
    • 判断每个关键域是否相等。
    public class EqualExample {
        private int x;
        private int y;
        private int z;
    
        public EqualExample(int x, int y, int z) {
            this.x = x;
            this.y = y;
            this.z = z;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
    
            EqualExample that = (EqualExample) o;
    
            if (x != that.x) return false;
            if (y != that.y) return false;
            return z == that.z;
        }
    }

    hashCode()

    hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。

    在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。

    下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hashCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。

    EqualExample e1 = new EqualExample(1, 1, 1);
    EqualExample e2 = new EqualExample(1, 1, 1);
    System.out.println(e1.equals(e2)); // true
    HashSet<EqualExample> set = new HashSet<>();
    set.add(e1);
    set.add(e2);
    System.out.println(set.size());   // 2

    理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。

    一个数与 31 相乘可以转换成移位和减法:31*x == (x<<5)-x,编译器会自动进行这个优化。

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + x;
        result = 31 * result + y;
        result = 31 * result + z;
        return result;
    }
    lambda表达式:函数的简写,也就是匿名函数
    格式:
    参数->结果(语句)
    
    interface Fun{
    double fun( double x );
    }
    double d = Integral_func(new Fun(){ public double fun(double x){
    return (x*9);
    } },
    0, 5); d = Integral_func(x->(x*9), 0, 5); Runnable doIt = new Runnable(){ public void run(){println("aaa");} }; new Thread( doIt ).start(); Runnable doIt2 = ()->println("bbb"); new Thread( doIt2 ).start(); new Thread( ()->println("ccc") ).start(); Comparator<Person> compA=(p1,p2)->p1.age-p2.age; Arrays.sort(people, compA); Arrays.sort(people, (p1,p2)->p1.age-p2.age);

    参数传递

    Java 的参数是以值传递的形式传入方法中,而不是引用传递。以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。

    如果在方法中使指针引用其它对象,那么这两个指针此时指向的是完全不同的对象,在一方改变其所指向对象的内容时对另一方没有影响。

    public class Dog {
        String name;
        Dog(String name) {
            this.name = name;
        }
        String getName() {
            return this.name;
        }
        void setName(String name) {
            this.name = name;
        }
        String getObjectAddress() {
            return super.toString();
        }
    }
    
    public class PassByValueExample {
        public static void main(String[] args) {
            Dog dog = new Dog("A");
            System.out.println(dog.getObjectAddress()); // Dog@4554617c   A
            func(dog);
            System.out.println(dog.getObjectAddress()); // Dog@4554617c   A
            System.out.println(dog.getName());          // A
        }
    
        private static void func(Dog dog) {
            System.out.println(dog.getObjectAddress()); // Dog@4554617c   A
            dog = new Dog("B");                         // 在方法中使指针引用到了其它对象
            System.out.println(dog.getObjectAddress()); // Dog@74a14482   B
            System.out.println(dog.getName());          // B
        }
    }

    如果在方法中改变对象的字段值则会改变原对象该字段值,因为改变的是同一个地址指向的内容。

    class PassByValueExample {
        public static void main(String[] args) {
            Dog dog = new Dog("A");
            func(dog);
            System.out.println(dog.getName());          // B
        }
    
        private static void func(Dog dog) {
            dog.setName("B");
        }
    }

    clone()

    cloneable

    clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。

    public class CloneExample {
        private int a;
        private int b;
    }
    
    CloneExample e1 = new CloneExample();
    // CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'

    重写 clone() 得到以下实现:

    public class CloneExample {
        private int a;
        private int b;
    
        @Override
        public CloneExample clone() throws CloneNotSupportedException {
            return (CloneExample)super.clone();
        }
    }
    
    CloneExample e1 = new CloneExample();
    try {
        CloneExample e2 = e1.clone();
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
    

    以上抛出了 java.lang.CloneNotSupportedException: CloneExample,这是因为 CloneExample 没有实现 Cloneable 接口。

     应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。

    public class CloneExample implements Cloneable {
        private int a;
        private int b;
    
        @Override
        public Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }

    浅拷贝:拷贝对象和原始对象的引用类型引用同一个对象。

    public class ShallowCloneExample implements Cloneable {
    
        private int[] arr;
    
        public ShallowCloneExample() {
            arr = new int[10];
            for (int i = 0; i < arr.length; i++) {
                arr[i] = i;
            }
        }
    
        public void set(int index, int value) {
            arr[index] = value;
        }
    
        public int get(int index) {
            return arr[index];
        }
    
        @Override
        protected ShallowCloneExample clone() throws CloneNotSupportedException {
            return (ShallowCloneExample) super.clone();
        }
    }
    
    ShallowCloneExample e1 = new ShallowCloneExample();
    ShallowCloneExample e2 = null;
    try {
        e2 = e1.clone();
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
    e1.set(2, 222);
    System.out.println(e2.get(2)); // 222

    深拷贝:拷贝对象和原始对象的引用类型引用不同对象。

    public class DeepCloneExample implements Cloneable {
    
        private int[] arr;
    
        public DeepCloneExample() {
            arr = new int[10];
            for (int i = 0; i < arr.length; i++) {
                arr[i] = i;
            }
        }
    
        public void set(int index, int value) {
            arr[index] = value;
        }
    
        public int get(int index) {
            return arr[index];
        }
    
        @Override
        protected DeepCloneExample clone() throws CloneNotSupportedException {
            DeepCloneExample result = (DeepCloneExample) super.clone();
            result.arr = new int[arr.length];
            for (int i = 0; i < arr.length; i++) {
                result.arr[i] = arr[i];
            }
            return result;
        }
    }
    
    DeepCloneExample e1 = new DeepCloneExample();
    DeepCloneExample e2 = null;
    try {
        e2 = e1.clone();
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
    e1.set(2, 222);
    System.out.println(e2.get(2)); // 2

    使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

    public class CloneConstructorExample {
    
        private int[] arr;
    
        public CloneConstructorExample() {
            arr = new int[10];
            for (int i = 0; i < arr.length; i++) {
                arr[i] = i;
            }
        }
    
        public CloneConstructorExample(CloneConstructorExample original) {
            arr = new int[original.arr.length];
            for (int i = 0; i < original.arr.length; i++) {
                arr[i] = original.arr[i];
            }
        }
    
        public void set(int index, int value) {
            arr[index] = value;
        }
    
        public int get(int index) {
            return arr[index];
        }
    }
    
    CloneConstructorExample e1 = new CloneConstructorExample();
    CloneConstructorExample e2 = new CloneConstructorExample(e1);
    e1.set(2, 222);
    System.out.println(e2.get(2)); // 2

    2. 一些关键字

    static

    静态变量:属于某个类,不属于某一实例

    • 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
    • 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死
    public class A {
        private int x;         // 实例变量
        private static int y;  // 静态变量
    
        public static void main(String[] args) {
            // int x = A.x;  // Non-static field 'x' cannot be referenced from a static context
            A a = new A();
            int x = a.x;
            int y = A.y;
        }
    }

    静态方法:不能访问实例变量

    静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
    
    public abstract class A {
        public static void func1(){
        }
        // public abstract static void func2();       // Illegal combination of modifiers: 'abstract' and 'static'
    }
    只能访问所属类的静态字段和静态方法,不能访问实例变量。方法中不能有
    thissuper 关键字。 public class A { private static int x; private int y; public static void func1(){ int a = x; // int b = y; // Non-static field 'y' cannot be referenced from a static context // int b = this.y; // 'A.this' cannot be referenced from a static context } }

    静态语句块:在类初始化时运行一次。

    public class A {
        static {
            System.out.println("123");
        }
        public static void main(String[] args) {
            A a1 = new A();
            A a2 = new A();
        }
    }
    
    
    输出:
    123

    静态内部类

    非静态内部类依赖于外部类的实例,而静态内部类不需要。

    public class OuterClass {
        class InnerClass {
        }
        static class StaticInnerClass {
        }
        public static void main(String[] args) {
            // InnerClass innerClass = new InnerClass();               // 'OuterClass.this' cannot be referenced from a static context
            OuterClass outerClass = new OuterClass();
            InnerClass innerClass = outerClass.new InnerClass();
            StaticInnerClass staticInnerClass = new StaticInnerClass();
        }
    }

    初始化顺序:在初始化一个类时,静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。

    public static String staticField = "静态变量";
    
    static {
        System.out.println("静态语句块");
    }
    
    public String field = "实例变量";
    
    {
        System.out.println("普通语句块");
    }
    
    最后才是构造函数的初始化。
    
    public InitialOrderTest() {
        System.out.println("构造函数");
    }

    存在继承的情况下,初始化顺序为:

    • 父类(静态变量、静态语句块)
    • 子类(静态变量、静态语句块)
    • 父类(实例变量、普通语句块)
    • 父类(构造函数)
    • 子类(实例变量、普通语句块)
    • 子类(构造函数)

    final

    final表示不可改变的,只能赋值一次。final类不可被继承。final方法不可被覆盖。

    变量

    final变量为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。

    • 对于基本类型,final 使数值不变;
    • 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
    final int x = 1;
    // x = 2;  // cannot assign value to final variable 'x'
    final A y = new A();
    y.a = 1;

    方法

    final方法不能被子类重写。

    private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。

    final类不允许被继承。

    final / finally / finalize

    • final用于声明属性方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。内部类要访问局部变量,局部变量必须定义成final类型。
    • finally是异常处理语句结构的一部分,表示总是执行。
    • finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其它资源回收,例如关闭文件等。JVM不保证此方法总被调用。

    3. 异常

    使用throw来抛出异常

    try{
      ....
    }
    catch (Exception e){
      ...
    }
    finally{    //有无异常都执行
      ...
    }

    Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: ErrorException。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:

    • 受检异常 :需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;
    • 非受检异常 :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复。

    4. 其他

    Java和C++的区别

    • Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
    • Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
    • Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
    • Java 支持自动垃圾回收,而 C++ 需要手动回收。
    • Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
    • Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
    • Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
    • Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。

    Java垃圾回收

    由JVM完成,使用引用计数

    垃圾回收时会自动调用对象的finalize()方法

    关于java虚拟机的详细知识参考  https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md

    内存中的栈/堆

    参考 https://www.cnblogs.com/pdev/p/11289870.html


    下一集:https://www.cnblogs.com/pdev/p/11289077.html

  • 相关阅读:
    Swift语法基础: 20
    Swift语法基础:19
    Swift语法基础:18
    Swift语法基础:17
    Swift语法基础:16
    Swift语法基础:15
    Swift语法基础:14
    Swift语法基础:13
    Swift语法基础:12
    Swift语法基础:11
  • 原文地址:https://www.cnblogs.com/pdev/p/11288014.html
Copyright © 2011-2022 走看看