From dca0212ac968fadc1f3c18ea69a5cf923e99a32e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 10 Aug 2019 23:44:22 +0800 Subject: [PATCH 001/323] =?UTF-8?q?Create=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 "Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" new file mode 100644 index 00000000..432a9db1 --- /dev/null +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -0,0 +1,3 @@ +# 1.1. Java 8种基本类型有哪些? +Byte short int long float double boolean char +![](https://ws4.sinaimg.cn/large/006Xmmmgly1g5v04dixr0j30d80ck0uu.jpg) From 444e053026c94479efc824f37831e9f85a246fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 10 Aug 2019 23:44:46 +0800 Subject: [PATCH 002/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 1 + 1 file changed, 1 insertion(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 432a9db1..bd7469c4 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -1,3 +1,4 @@ # 1.1. Java 8种基本类型有哪些? Byte short int long float double boolean char + ![](https://ws4.sinaimg.cn/large/006Xmmmgly1g5v04dixr0j30d80ck0uu.jpg) From 751071d2f4d95891b869ef445581f594bc6e4bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 10 Aug 2019 23:52:49 +0800 Subject: [PATCH 003/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...7\202\271\345\222\214\347\255\224\346\241\210.md" | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index bd7469c4..1837b73f 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -2,3 +2,15 @@ Byte short int long float double boolean char ![](https://ws4.sinaimg.cn/large/006Xmmmgly1g5v04dixr0j30d80ck0uu.jpg) + +# 1.2 什么是装箱和拆箱? +装箱就是 自动将基本数据类型转换为包装器类型;拆箱就是 自动将包装器类型转换为基本数据类型。 +比如:把int转化成Integer,double转化成Double,等等。反之就是自动拆箱。 +- 原始类型: boolean,char,byte,short,int,long,float,double +- 封装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double +示例: +Integer i = 10; +这个过程中会自动根据数值创建对应的 Integer对象,这就是装箱。 +那什么是拆箱呢?顾名思义,跟装箱对应,就是自动将包装器类型转换为基本数据类型: +Integer i = 10; //装箱 +int n = i; //拆箱 From 71b107bbf338351bf117cab897c7d84ec15a7b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 10 Aug 2019 23:53:12 +0800 Subject: [PATCH 004/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 1837b73f..9896b4e2 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -8,7 +8,7 @@ Byte short int long float double boolean char 比如:把int转化成Integer,double转化成Double,等等。反之就是自动拆箱。 - 原始类型: boolean,char,byte,short,int,long,float,double - 封装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double -示例: +- 示例: Integer i = 10; 这个过程中会自动根据数值创建对应的 Integer对象,这就是装箱。 那什么是拆箱呢?顾名思义,跟装箱对应,就是自动将包装器类型转换为基本数据类型: From df6cc6c88df2c8876517d4ea02d1194f33c9b97d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 10 Aug 2019 23:55:06 +0800 Subject: [PATCH 005/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 2 ++ 1 file changed, 2 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 9896b4e2..ddc68f3d 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -9,8 +9,10 @@ Byte short int long float double boolean char - 原始类型: boolean,char,byte,short,int,long,float,double - 封装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double - 示例: +``` Integer i = 10; 这个过程中会自动根据数值创建对应的 Integer对象,这就是装箱。 那什么是拆箱呢?顾名思义,跟装箱对应,就是自动将包装器类型转换为基本数据类型: Integer i = 10; //装箱 int n = i; //拆箱 +``` From 00acfdad37523a96af5658b4b7aa50ddce4bbb6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sun, 11 Aug 2019 00:12:25 +0800 Subject: [PATCH 006/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...71\345\222\214\347\255\224\346\241\210.md" | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index ddc68f3d..66ce8af5 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -16,3 +16,58 @@ Integer i = 10; Integer i = 10; //装箱 int n = i; //拆箱 ``` +# 1.3 short s1 = 1; s1 = s1 + 1;有什么错? short s1 = 1; s1 +=1;有什么错? + +- 对于short s1=1;s1=s1+1来说,在s1+1运算时会自动提升表达式的类型为int,那么将int赋予给short类型的变量s1会出现类型转换错误。 +- 对于short s1=1;s1+=1来说 +=是java语言规定的运算符,java编译器会对它进行特殊处理,因此可以正确编译。 + +# 1.4 int和Integer的区别 + +https://www.cnblogs.com/guodongdidi/p/6953217.html + +- Integer是int的包装类,int则是java的一种基本数据类型 +- Integer变量必须实例化后才能使用,而int变量不需要 +- Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值 +- Integer的默认值是null,int的默认值是0 + +延伸: +关于Integer和int的比较 +1、由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。 +``` +Integer i = new Integer(100); +Integer j = new Integer(100); +System.out.print(i == j); //false +``` +2、Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较) +``` +Integer i = new Integer(100); +int j = 100; +System.out.print(i == j); //true +``` +3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同) +``` +Integer i = new Integer(100); +Integer j = 100; +System.out.print(i == j); //false +``` +4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false +``` +Integer i = 100; +Integer j = 100; +System.out.print(i == j); //true +Integer i = 128; +Integer j = 128; +System.out.print(i == j); //false +``` +对于第4条的原因: +java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);,而java API中对Integer类型的valueOf的定义如下: +``` +public static Integer valueOf(int i){ + assert IntegerCache.high >= 127; + if (i >= IntegerCache.low && i <= IntegerCache.high){ + return IntegerCache.cache[i + (-IntegerCache.low)]; + } + return new Integer(i); +} +``` +java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了 From b87b439d4c8915c98a0932d40c526fd14bf9baf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sun, 11 Aug 2019 21:39:39 +0800 Subject: [PATCH 007/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...7\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 5 +++++ 1 file changed, 5 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 66ce8af5..d9ee37d1 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -71,3 +71,8 @@ public static Integer valueOf(int i){ } ``` java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了 +# 1.5 字节和字符的区别 +字节是存储容量的基本单位,字符是数子,字母,汉子以及其他语言的各种符号。1字节=8个二进制单位:一个一个字符由一个字节或多个字节的二进制单位组成。 +# 1.6 基本类型和引用类型的区别 +基本类型保存原始值,引用类型保存的是引用值(引用值就是指对象在堆中所处的位置/地址) +# 1.7 From 448fb045802d0577f3609ef03791e8af853df634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sun, 11 Aug 2019 21:57:42 +0800 Subject: [PATCH 008/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...271\345\222\214\347\255\224\346\241\210.md" | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index d9ee37d1..09f1c64f 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -75,4 +75,20 @@ java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将 字节是存储容量的基本单位,字符是数子,字母,汉子以及其他语言的各种符号。1字节=8个二进制单位:一个一个字符由一个字节或多个字节的二进制单位组成。 # 1.6 基本类型和引用类型的区别 基本类型保存原始值,引用类型保存的是引用值(引用值就是指对象在堆中所处的位置/地址) -# 1.7 +# 1.7 Java的四个基本特性及多态的理解? +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w2pliuhij30js0b8n1z.jpg) +# 1.8 重载和重写的区别? +## 1.8.1 重载 + +https://blog.csdn.net/cey009008/article/details/46331619 + + 重载(overload),Java中的方法重载发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同的情况。 +-1.可以在一个类中也可以在继承关系的类中; +-2.名相同; +- 3.参数列表不同(个数,顺序,类型) 和方法的返回值类型无关。 +## 1.8.2 重写 +重写(override)又名覆盖,方法覆盖是说子类重新定义了父类的方法。方法覆盖必须有相同的方法名,参数列表和返回类型。覆盖者可能不会限制它所覆盖的方法的访问。: +- 1.不能存在同一个类中,在继承或实现关系的类中; +- 2.名相同,参数列表相同,方法返回值相同, +- 3.子类方法的访问修饰符要大于父类的。 +- 4.子类的检查异常类型要小于父类的检查异常。 From a757b14b38093f4999b6ccd702265d6b2db36008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sun, 11 Aug 2019 22:21:48 +0800 Subject: [PATCH 009/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\202\271\345\222\214\347\255\224\346\241\210.md" | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 09f1c64f..7035288d 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -92,3 +92,16 @@ https://blog.csdn.net/cey009008/article/details/46331619 - 2.名相同,参数列表相同,方法返回值相同, - 3.子类方法的访问修饰符要大于父类的。 - 4.子类的检查异常类型要小于父类的检查异常。 +# 1.9 Java中是否可以覆盖(override)一个private或者是static的方法? +Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。java中也不可以覆盖private的方法,因为private修饰的变量和方法只能在当前类中使用,如果是其他的类继承当前类是不能访问到private变量或方法的,当然也不能覆盖。 +# 1.10 面向对象的六个基本原则 +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w39fm22gj30vw0hy13y.jpg) +# 1.11 Java 创建对象的四种方式 +- 1.使用new创建对象 +使用new关键字创建对象应该是最常见的一种方式,但我们应该知道,使用new创建对象会增加耦合度。无论使用什么框架,都要减少new的使用以降低耦合度。 +- 2.使用反射的机制创建对象 +使用Class类的newInstance方法 +- 3.采用clone +clone时,需要已经有一个分配了内存的源对象,创建新对象时,首先应该分配一个和源对象一样大的内存空间。要调用clone方法需要实现Cloneable接口 +- 4. 采用序列化机制 +使用序列化时,要实现实现Serializable接口,将一个对象序列化到磁盘上,而采用反序列化可以将磁盘上的对象信息转化到内存中。 From e23eaaa54adfafdb9d63f984e0abb965932e39c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sun, 11 Aug 2019 22:59:08 +0800 Subject: [PATCH 010/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...71\345\222\214\347\255\224\346\241\210.md" | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 7035288d..8d251080 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -97,11 +97,106 @@ Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态 # 1.10 面向对象的六个基本原则 ![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w39fm22gj30vw0hy13y.jpg) # 1.11 Java 创建对象的四种方式 +https://www.cnblogs.com/yunche/p/9530927.html - 1.使用new创建对象 使用new关键字创建对象应该是最常见的一种方式,但我们应该知道,使用new创建对象会增加耦合度。无论使用什么框架,都要减少new的使用以降低耦合度。 - 2.使用反射的机制创建对象 使用Class类的newInstance方法 - 3.采用clone clone时,需要已经有一个分配了内存的源对象,创建新对象时,首先应该分配一个和源对象一样大的内存空间。要调用clone方法需要实现Cloneable接口 -- 4. 采用序列化机制 +- 4.采用序列化机制 使用序列化时,要实现实现Serializable接口,将一个对象序列化到磁盘上,而采用反序列化可以将磁盘上的对象信息转化到内存中。 +# 1.12 String、StringBuffer和StringBuilder的区别 +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w4mh9b0zj30ur06pn0z.jpg) +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w4mlg6vej30fe016aa8.jpg) +# 1.13 String不可变好处 +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w4oo4m9qj30q60cln0f.jpg) +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w4osa6aij30fe03pgmi.jpg) + +# 1.14 String Pool + +https://github.com/gzc426/CS-Notes/edit/master/docs/notes/Java%20%E5%9F%BA%E7%A1%80.md + +字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。 + +当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。 + +下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。 + +```java +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 中。 + +```java +String s5 = "bbb"; +String s6 = "bbb"; +System.out.println(s5 == s6); // true +``` + +在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。 + +- [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning) +- [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html) + +# 1.15 new String("abc") + +使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。 + +- "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量; +- 而使用 new 的方式会在堆中创建一个字符串对象。 + +创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。 + +```java +public class NewStringTest { + public static void main(String[] args) { + String s = new String("abc"); + } +} +``` + +使用 javap -verbose 进行反编译,得到以下内容: + +```java +// ... +Constant pool: +// ... + #2 = Class #18 // java/lang/String + #3 = String #19 // abc +// ... + #18 = Utf8 java/lang/String + #19 = Utf8 abc +// ... + + public static void main(java.lang.String[]); + descriptor: ([Ljava/lang/String;)V + flags: ACC_PUBLIC, ACC_STATIC + Code: + stack=3, locals=2, args_size=1 + 0: new #2 // class java/lang/String + 3: dup + 4: ldc #3 // String abc + 6: invokespecial #4 // Method java/lang/String."":(Ljava/lang/String;)V + 9: astore_1 +// ... +``` + +在 Constant Pool 中,#19 存储这字符串字面量 "abc",#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。 + +以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。 + +```java +public String(String original) { + this.value = original.value; + this.hash = original.hash; +} +``` +# 1.16 String中的hashcode以及toString +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w4sq64ymj30xf0bvtfd.jpg) From 9b5b97a9054730a9d9e20ca6190525333fdf4d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sun, 11 Aug 2019 23:14:22 +0800 Subject: [PATCH 011/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 3 +++ 1 file changed, 3 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 8d251080..36fbd1f6 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -111,6 +111,7 @@ clone时,需要已经有一个分配了内存的源对象,创建新对象时 ![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w4mlg6vej30fe016aa8.jpg) # 1.13 String不可变好处 ![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w4oo4m9qj30q60cln0f.jpg) + ![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w4osa6aij30fe03pgmi.jpg) # 1.14 String Pool @@ -200,3 +201,5 @@ public String(String original) { ``` # 1.16 String中的hashcode以及toString ![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w4sq64ymj30xf0bvtfd.jpg) + + From f4e1b6cffbe93ab335f58769d9f6c5f94cb54bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sun, 11 Aug 2019 23:37:32 +0800 Subject: [PATCH 012/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...71\345\222\214\347\255\224\346\241\210.md" | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 36fbd1f6..22bc65f7 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -199,7 +199,34 @@ public String(String original) { this.hash = original.hash; } ``` -# 1.16 String中的hashcode以及toString +# 1.16 string有重写 Object的 hashcode和 toString吗?如果重写 equals不重写 hashcode会出现什么问题 ![](http://ww1.sinaimg.cn/large/007s8HJUly1g5w4sq64ymj30xf0bvtfd.jpg) +在存储散列集合时〔如Set类),如果原对象equals新对象,但没有对 hashCode重写,即两个对象拥有不同的hashCode,则在集合中将会存储两个值相同的对象,从而导致混看。因此在重写 equals方法时,必须重写 hashCode +# 1.17 String中的“+”是如何实现的? +https://www.cnblogs.com/xiaoxi/p/6036701.html +``` +String c = "xx" + "yy " + a + "zz" + "mm" + b; 实质上的实现过程是: String c = new StringBuilder("xxyy ").append(a).append("zz").append("mm").append(b).toString(); +底层通过 StringBuilder实现 +``` +# 1.18 Java 如何进行文件读取? + +1.首先获得一个文件句柄。 File fille= new File(;file即为文件句柄。两人之间连 + +通电话网络了。接下来可以开始打电话了 + + +2.通过这条线路读取甲方的信息: new FileInputStream(fe)目前这个信息已经 + +读进来内存当中了。接下来需要解读成乙方可以理解的东西 + +3.既然你使用了 FileInputStream()。那么对应的需要使用 InputStream Reader( + +这个方法进行解读刚才装进来内存当中的数据 + + +4.解读完成后要输出呀。那当然要转换成ⅠO可以识别的数据呀。那就需要调用字节 + +码读取的方法 Bufferedreader()。同时使用 buffered Reader(的 readline()方 +法读取txt文件中的每一行数据哈。 From 12561e1a3c8d7259827789818d8a1c703a3a9e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sun, 11 Aug 2019 23:41:27 +0800 Subject: [PATCH 013/323] =?UTF-8?q?Create=20=E5=89=91=E6=8C=87offer?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=EF=BC=88Java=E5=AE=9E=E7=8E=B0=EF=BC=89.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...va\345\256\236\347\216\260\357\274\211.md" | 4087 +++++++++++++++++ 1 file changed, 4087 insertions(+) create mode 100644 "\345\211\221\346\214\207offer\350\247\243\346\236\220\357\274\210Java\345\256\236\347\216\260\357\274\211.md" diff --git "a/\345\211\221\346\214\207offer\350\247\243\346\236\220\357\274\210Java\345\256\236\347\216\260\357\274\211.md" "b/\345\211\221\346\214\207offer\350\247\243\346\236\220\357\274\210Java\345\256\236\347\216\260\357\274\211.md" new file mode 100644 index 00000000..57817d11 --- /dev/null +++ "b/\345\211\221\346\214\207offer\350\247\243\346\236\220\357\274\210Java\345\256\236\347\216\260\357\274\211.md" @@ -0,0 +1,4087 @@ +--- +title: 剑指offer解析(Java实现) +date: 2019-01-18 18:32:16 +updated_at: +comments: true +photos: "" +categories: 数据结构与算法 +tags: 数据结构与算法 +--- + +> 以下题目按照牛客网在线编程排序,所有代码示例代码均已通过牛客网OJ。 + +### 二维数组的查找 + +#### 题目描述 + +在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 + +```java +public boolean Find(int target, int [][] arr) { + +} +``` + + + +#### 解析 + +暴力方法是遍历一遍二维数组,找到`target`就返回`true`,时间复杂度为`O(M * N)`(对于M行N列的二维数组)。 + +由题可知输入的数据样本具有高度规律性(单独一行的数据来看是有序的,单独一列的数据来看也是有序的),因此考虑能否有一个比较基准在一次的比较中根据有序性淘汰不必再进行遍历比较的数。**有序**、**查找**,由此不难联想到二分查找,我们可以借鉴二分查找的思路,每次选出一个数作为比较基准进而淘汰掉一些不必比较的数。二分是选取数组的中位数作为比较基准的,因此能够保证每次都淘汰掉二分之一的数,那么此题中有没有这种特性的数呢?我们不妨举例观察一下: + +![image](https://ws1.sinaimg.cn/large/006zweohgy1fzaxy3978uj304x04mt8m.jpg) + +不难发现上图中对角线上的数是其所在行和所在列形成的序列的中位数,不妨选取右上角的数作为比较基准,如果不相等,那么我们可以淘汰掉所有它左边的数或者它所有下面的,比如对于`target = 6`,因为`(0,3)`位置上的`4 < 6`,因此`(0,3)`位置及其同一行的左边的所有数都小于6因此可以直接淘汰掉,淘汰掉之后问题就变为了从剩下的三行中找`target`,这与原始问题是相似的,也就是说每一次都选取右上角的数据为比较基准然后淘汰掉一行或一列,直到某一轮被选取的数就是`target`或者已经淘汰得只剩下一个数的时候就一定能得出结果了,因此时间复杂度为被淘汰掉的行数和列数之和,即`O(M + N)`,经过分析后不难写出如下代码: + +```java +public boolean Find(int target, int [][] arr) { + //input check + if(arr == null || arr.length == 0 || arr[0] == null || arr[0].length == 0){ + return false; + } + int i = 0, j = arr[0].length - 1; + while(i != arr.length - 1 && j != 0){ + if(target > arr[i][j]){ + i++; + }else if(target < arr[i][j]){ + j--; + }else{ + return true; + } + } + + return target == arr[i][j]; +} +``` + +值得注意的是每次选取的数都是第一行最后一个数,因此前提是第一行有数,那么就对应着输入检查的`arr[0] == null || arr[0].length == 0`,这点比较容易忽略。 + +> 总结:经过分析其实不难发现,此题是在一维有序数组使用二分查找元素的一个变种,我们应该充分利用数据本身的规律性来寻找解题思路。 + +### 替换空格 + +#### 题目描述 + +请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。 + +```java +public String replaceSpace(StringBuffer str) { + +} +``` + + + +> 此题考查的是字符串这个数据结构的数组实现(对应的还有链表实现)的相关操作。 + +#### 解析 + +##### String.replace简单粗暴 + +如果可以使用`API`,那么可以很容易地写出如下代码: + +```java +public String replaceSpace(StringBuffer str) { + //input check + //null pointer + if(str == null){ + return null; + } + //empty str or not exist blank + if(str.length() == 0 || str.indexOf(" ") == -1){ + return str.toString(); + } + + for(int i = 0 ; i < str.length() ; i++){ + if(str.charAt(i) == ' '){ + str.replace(i, i + 1, "%20"); + } + } + + return str.toString(); +} +``` + +##### 时间O(n),空间O(n) + +但是如果面试官告诉我们不许使用封装好的替换函数,那么目的就是在考查我们对字符串**数组实现**方式的相关操作。由于是连续空间存储,因此需要在创建实例时指定大小,由于每个空格都使用`%20`替换,因此替换之后的字符串应该比原串多出`空格数 * 2`个长度,实现如下: + +```java +public String replaceSpace(StringBuffer str) { + //input check + //null pointer + if(str == null){ + return null; + } + //empty str or not exist blank + if(str.length() == 0 || str.indexOf(" ") == -1){ + return str.toString(); + } + + char[] source = str.toString().toCharArray(); + int blankCount = 0; + for(int i = 0 ; i < source.length ; i++){ + blankCount = (source[i] == ' ') ? blankCount + 1 : blankCount; + } + char[] dest = new char[source.length + blankCount * 2]; + for(int i = source.length - 1, j = dest.length - 1 ; i >=0 && j >=0 ; i--, j--){ + if(source[i] == ' '){ + dest[j--] = '0'; + dest[j--] = '2'; + dest[j] = '%'; + continue; + }else{ + dest[j] = source[i]; + } + } + + return new String(dest); +} +``` + +##### 时间O(n),空间O(1) + +如果还要求不能有额外空间,那我们就要考虑如何复用输入的字符串,如果我们从前往后遇到空格就将空格及其之后的两个位置替换为`%20`,势必会覆盖空格之后的两个字符,比如`hello world`会被替换成`hello%20rld`,因此我们需要在长度被扩展后的新串中从后往前确定每个索引上的字符。比如使用一个`originalIndex`指向原串中的最后一个字符索引,使用`newIndex`指向新串的最后一个索引,每次将`originalIndex`上的字符复制到`newIndex`上并且两个指针前移,如果`originalIndex`上的字符是空格,则将`newIndex`依次填充`0,2,%`,然后两者再前移,直到两者都到首索引位置。 + +![image](https://ws1.sinaimg.cn/large/006zweohgy1fzb0mknemyj30ng0df74w.jpg) + +```java +public String replaceSpace(StringBuffer str) { + //input check + //null pointer + if(str == null){ + return null; + } + //empty str or not exist blank + if(str.length() == 0 || str.indexOf(" ") == -1){ + return str.toString(); + } + + int blankCount = 0; + for(int i = 0 ; i < str.length() ; i++){ + blankCount = (str.charAt(i) == ' ') ? blankCount + 1 : blankCount; + } + int originalIndex = str.length() - 1, newIndex = str.length() - 1 + blankCount * 2; + str.setLength(newIndex + 1); //需要重新设置一下字符串的长度,否则会报越界错误 + while(originalIndex >= 0 && newIndex >= 0){ + if(str.charAt(originalIndex) == ' '){ + str.setCharAt(newIndex--, '0'); + str.setCharAt(newIndex--, '2'); + str.setCharAt(newIndex, '%'); + }else{ + str.setCharAt(newIndex, str.charAt(originalIndex)); + } + originalIndex--; + newIndex--; + } + + return str.toString(); +} +``` + +> 总结:要把思维打开,对于数组的操作我们习惯性的以`for(int i = 0 ; i < arr.length ; i++)`的形式从头到尾来操作数组,但是不要忽略了从尾到头遍历也有它的独到之处。 + +### 反转链表 + +#### 题目描述 + +输入一个链表,反转链表后,输出新链表的表头。 + +```java +public ListNode ReverseList(ListNode head) { + +} +``` + + + +#### 解析 + +此题的难点在于无法通过一个单链表结点获取其前驱结点,因此我们不仅要在反转指针之前保存当前结点的前驱结点,还要保存当前结点的后继结点,并在下一次反转之前更新这两个指针。 + +```java +/* +public class ListNode { + int val; + ListNode next = null; + + ListNode(int val) { + this.val = val; + } +}*/ +public ListNode ReverseList(ListNode head) { + if(head == null || head.next == null){ + return head; + } + ListNode pre = null, p = head, next; + while(p != null){ + next = p.next; + p.next = pre; + pre = p; + p = next; + } + + return pre; +} +``` + + + +### 从尾到头打印链表 + +#### 题目描述 + +输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。 + +```java +public ArrayList printListFromTailToHead(ListNode listNode) { + +} +``` + + + +#### 解析 + +此题的难点在于单链表只有指向后继结点的指针,因此我们无法通过当前结点获取前驱结点,因此不要妄想先遍历一遍链表找到尾结点然后再依次从后往前打印。 + +##### 递归,简洁优雅 + +由于我们通常是从头到尾遍历链表的,而题目要求从尾到头打印结点,这与前进后出的逻辑是相符的,因此你可以使用一个栈来保存遍历时走过的结点,再通过后进先出的特性实现从尾到头打印结点,但是我们也可以利用递归来帮我们压栈,由于递归简洁不易出错,因此面试中能用递归尽量用递归:只要当前结点不为空,就递归遍历后继结点,当后继结点为空时,递归结束,在递归回溯时将“当前结点”依次添加到集合中 + +```java +/** +* public class ListNode { +* int val; +* ListNode next = null; +* +* ListNode(int val) { +* this.val = val; +* } +* } +* +*/ +import java.util.ArrayList; +public class Solution { + public ArrayList printListFromTailToHead(ListNode listNode) { + ArrayList res = new ArrayList(); + //input check + if(listNode == null){ + return res; + } + recursively(res, listNode); + return res; + } + + public void recursively(ArrayList res, ListNode node){ + //base case + if(node == null){ + return; + } + //node not null + recursively(res, node.next); + res.add(node.val); + return; + } +} +``` + +##### 反转链表 + +还有一种方法就是将链表指针都反转,这样将反转后的链表从头到尾打印就是结果了。需要注意的是我们不应该在访问用户数据时更改存储数据的结构,因此最后要记得反转回来: + +```java +public ArrayList printListFromTailToHead(ListNode listNode) { + ArrayList res = new ArrayList(); + //input check + if(listNode == null){ + return res; + } + return unrecursively(listNode); +} + +public ArrayList unrecursively(ListNode node){ + ArrayList res = new ArrayList(); + ListNode newHead = reverse(node); + ListNode p = newHead; + while(p != null){ + res.add(p.val); + p = p.next; + } + reverse(newHead); + return res; +} + +public ListNode reverse(ListNode node){ + ListNode pre = null, cur = node, next; + while(cur != null){ + //save predecessor + next = cur.next; + //reverse pointer + cur.next = pre; + //move to next + pre = cur; + cur = next; + } + //cur is null + return pre; +} +``` + +> 总结:面试时能用递归就用递归,当然了如果面试官就是要考查你的指针功底那你也能`just so so`不是 + +### 重建二叉树 + +#### 题目描述 + +输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,2,7,1,5,3,8,6},则重建二叉树并返回。 + +```java +public TreeNode reConstructBinaryTree(int [] pre,int [] in) { + +} +``` + + + +#### 解析 + +先序序列的特点是第一个数就是根结点而后是左子树的先序序列和右子树的先序序列,而中序序列的特点是先是左子树的中序序列,然后是根结点,最后是右子树的中序序列。因此我们可以通过先序序列得到根结点,然后通过在中序序列中查找根结点的索引从而得到左子树和右子树的结点数。然后可以将两序列都一分为三,对于其中的根结点能够直接重建,然后根据对应子序列分别递归重建根结点的左子树和右子树。这是一个典型的将复杂问题划分成子问题分步解决的过程。 + +![image](https://ws2.sinaimg.cn/large/006zweohgy1fzb43dddiej30f70azjrt.jpg) + +递归体的定义,如上图先序序列的左子树序列是`2,3,4`对应下标`1,2,3`,而中序序列的左子树序列是`3,2,4`对应下标`0,1,2`,因此递归体接收的参数除了保存两个序列的数组之外,还需要指明需要递归重建的子序列分别在两个数组中的索引范围:`TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n)`。然后递归体根据`pre`的`i~j`索引范围形成的先序序列和`in`的`m~n`索引范围形成的中序序列重建一棵树并返回根结点。 + +首先根结点就是先序序列的第一个数,即`pre[i]`,因此`TreeNode root = new TreeNode(pre[i])`可以直接确定,然后通过在`in`的`m~n`中查找出`pre[i]`的索引`index`可以求得左子树结点数`leftNodes = index - m`,右子树结点数`rightNodes = n - index`,如果左(右)子树结点数为0则表明左(右)子树为`null`,否则通过`root.left = rebuild(pre, i' ,j' ,in ,m' ,n')`来重建左(右)子树即可。 + +这个题的难点也就在这里,即`i',j',m',n'`的值的确定,笔者曾在此困惑许久,建议通过`leftNodes,rightNodes`和`i,j,m,n`来确定:(这个时候了前往不要在脑子里面想这些下标对应关系!!一定要在纸上画,确保准确性和概括性) + +![image](https://wx3.sinaimg.cn/large/006zweohgy1fzbmo2052rj309v088dfy.jpg) + +于是容易得出如下代码: + +```java +if(leftNodes == 0){ + root.left = null; +}else{ + root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1); +} +if(rightNodes == 0){ + root.right = null; +}else{ + root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n); +} +``` + +笔者曾以中序序列的根节点索引来确定`i',j',m',n'`的对应关系写出如下**错误代码**: + +![image](https://ws4.sinaimg.cn/large/006zweohgy1fzbmvcv9yej306b07adfv.jpg) + +```java +if(leftNodes == 0){ + root.left = null; +}else{ + root.left = rebuild(pre, i + 1, index, in, m, index - 1); +} +if(rightNodes == 0){ + root.right = null; +}else{ + root.right = rebuild(pre, index + 1, j, in, index + 1, n); +} +``` + +这种对应关系乍一看没错,但是不具有概括性(即囊括所有情况),比如对序列`2,3,4`、`3,2,4`重建时: + +![image](https://wx1.sinaimg.cn/large/006zweohgy1fzbn2rz5n5j30cv07v3yh.jpg) + +你看这种情况,上述错误代码还适用吗?原因就在于`index`是在`in`的`m~n`中选取的,与数组`in`是绑定的,和`pre`没有直接的关系,因此如果用`index`来表示`i',j'`自然是不合理的。 + +此题的正确完整代码如下: + +```java +/** + * Definition for binary tree + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode(int x) { val = x; } + * } + */ +public class Solution { + public TreeNode reConstructBinaryTree(int [] pre,int [] in) { + if(pre == null || in == null || pre.length == 0 || in.length == 0 || pre.length != in.length){ + return null; + } + return rebuild(pre, 0, pre.length - 1, in, 0, in.length - 1); + } + + public TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n){ + int rootVal = pre[i], index = findIndex(rootVal, in, m, n); + if(index < 0){ + return null; + } + int leftNodes = index - m, rightNodes = n - index; + TreeNode root = new TreeNode(rootVal); + if(leftNodes == 0){ + root.left = null; + }else{ + root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1); + } + if(rightNodes == 0){ + root.right = null; + }else{ + root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n); + } + return root; + } + + public int findIndex(int target, int arr[], int from, int to){ + for(int i = from ; i <= to ; i++){ + if(arr[i] == target){ + return i; + } + } + return -1; + } +} +``` + +> 总结: +> +> 1. 对于复杂问题,一定要划分成若干子问题,逐一求解。比如二叉树问题,我们通常将其划分成头结点、左子树、右子树。 +> 2. 对于递归过程的参数对应关系,尽量使用和数据样本本身没有直接关系的变量来表示。比如此题应该选取`leftNodes`和`rightNodes`来计算`i',j',m',n'`而不应该使用头结点在中序序列的下标`index`(它和`in`是绑定的,那么可能对`pre`就不适用了)。 + +### 用两个栈实现队列 + +#### 题目描述 + +用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。 + +```java +Stack stack1 = new Stack(); +Stack stack2 = new Stack(); + +public void push(int node) { + +} + +public int pop() { + +} +``` + + + +#### 解析 + +这道题只要记住以下几点即可: + +1. 一个栈(如`stack1`)只能用来存,另一个栈(如`stack2`)只能用来取 +2. 当取元素时首先检查`stack2`是否为空,如果不空直接`stack2.pop()`,否则将`stack1`中的元素**全部倒入**`stack2`,如果倒入之后`stack2`仍为空则需要抛异常,否则`stack2.pop()`。 + +代码示例如下: + +```java +import java.util.Stack; + +public class Solution { + Stack stack1 = new Stack(); + Stack stack2 = new Stack(); + + public void push(int node) { + stack1.push(node); + } + + public int pop() { + if(stack2.empty()){ + while(!stack1.empty()){ + stack2.push(stack1.pop()); + } + } + if(stack2.empty()){ + throw new IllegalStateException("no more element!"); + } + return stack2.pop(); + } +} +``` + +> 总结:只要取元素的栈不为空,取元素时直接弹出其栈顶元素即可,只有当其为空时才考虑将存元素的栈倒入进来,并且要一次性倒完。 + +### 旋转数组的最小数字 + +#### 题目描述 + +把一个数组最开始的**若干**个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个**非减排序**的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。 + +```java +public int minNumberInRotateArray(int [] arr) { + +} +``` + + + +#### 解析 + +此题需先认真审题: + +1. 若干,涵盖了一个元素都不搬的情况,此时数组是一个非减排序序列,因此首元素就是数组的最小元素。 +2. 非减排序,并不代表是递增的,可能会出现若干相邻元素相同的情况,极端的例子是整个数组的所有元素都相同 + +由此不难得出如下`input check`: + +```java +public int minNumberInRotateArray(int [] arr) { + //input check + if(arr == null || arr.length == 0){ + return 0; + } + //if only one element or no rotate + if(arr.length == 1 || arr[0] < arr[arr.length - 1]){ + return arr[0]; + } + + //TODO +} +``` + +上述的`arr[0] < arr[arr.length - 1]`不能写成`arr[0] <= arr[arr.length - 1]`,比如可能会有`[1,2,3,3,4] -> [3,4,1,2,3]` 的情况,这时你不能返回`arr[0]=3`。 + +如果走到了程序中的`TODO`,就可以考虑普遍情况下的推敲,数组可以被分成两部分:大于等于`arr[0]`的左半部分和小于等于`arr[arr.length - 1]`右半部分,我们不妨借助两个指针从数组的头、尾向中间靠近,这样就能利用二分的思想快速移动指针从而淘汰一些不在考虑范围之内的数。 + +![image](https://wx2.sinaimg.cn/large/006zweohgy1fzbpp2dx1dj30a0063aa1.jpg) + +如图,我们不能直接通过`arr[mid]`和`arr[l]`(或`arr[r]`)的比较(`arr[mid] >= arr[l]`)来决定移动`l`还是`r`到`mid`上,因为数组可能存在若干相同且相邻的数,因此我们还需要加上一个限制条件:`arr[l + 1] >= arr[l] && arr[mid] >= arr[l]`(对于`r`来说则是`arr[r - 1] <= arr[r] && arr[mid] <= arr[r]`),即当左半部分(右半部分)不止一个数时,我们才可能去移动`l`(`r`)指针。完整代码如下: + +```java +import java.util.ArrayList; +public class Solution { + public int minNumberInRotateArray(int [] arr) { + //input check + if(arr == null || arr.length == 0){ + return 0; + } + //if only one element or no rotate + if(arr.length == 1 || arr[0] < arr[arr.length - 1]){ + return arr[0]; + } + + //has rotate, left part is big than right part + int l = 0, r = arr.length - 1, mid; + //l~r has more than 3 elements + while(r > l && r - l != 1){ + //r-l >= 2 -> mid > l + mid = l + ((r - l) >> 1); + if(arr[l + 1] >= arr[l] && arr[mid] >= arr[l]){ + l = mid; + }else{ + r = mid; + } + } + + return arr[r]; + } +} +``` + +> 总结:审题时要充分考虑数据样本的极端情况,以写出鲁棒性较强的代码。 + +### 斐波那契数列 + +#### 题目描述 + +大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39 + +```java +public int Fibonacci(int n) { + +} +``` + + + +#### 解析 + +##### 递归方式 + +对于公式`f(n) = f(n-1) + f(n-2)`,明显就是一个递归调用,因此根据`f(0) = 0`和`f(1) = 1`我们不难写出如下代码: + +```java +public int Fibonacci(int n) { + if(n == 0 || n == 1){ + return n; + } + return Fibonacci(n - 1) + Fibonacci(n - 2); +} +``` + +##### 动态规划 + +在上述递归过程中,你会发现有很多计算过程是重复的: + +![image](https://ws1.sinaimg.cn/large/006zweohgy1fzbq4avws3j30b507b74a.jpg) + +**动态规划就在使用递归调用自上而下分析过程中发现有很多重复计算的子过程,于是采用自下而上的方式将每个子状态缓存下来,这样对于上层而言只有当需要的子过程结果不在缓存中时才会计算一次,因此每个子过程都只会被计算一次**。 + +```java +public int Fibonacci(int n) { + if(n == 0 || n == 1){ + return n; + } + //n1 -> f(n-1), n2 -> f(n-2) + int n1 = 1, n2 = 0; + //从f(2)开始算起 + int N = 2, res = 0; + while(N++ <= n){ + //每次计算后更新缓存,当然你也可以使用一个一维数组保存每次的计算结果,只额外空间复杂度就变为O(n)了 + res = n1 + n2; + n2 = n1; + n1 = res; + } + return res; +} +``` + +上述代码很多人都能写出来,只是没有意识到这就是动态规划。 + +> 总结:当你自上而下分析递归时发现有很多子过程被重复计算,那么就应该考虑能否通过自下而上将每个子过程的计算结果缓存下来。 + +### 跳台阶 + +#### 题目描述 + +一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。 + +```java +public int JumpFloor(int target) { + +} +``` + + + +#### 解析 + +##### 递归版本 + +将复杂问题分解:复杂问题就是不断地将`target`减1或减2(对应跳一级和跳两级台阶)直到`target`变为1或2(对应只剩下一层或两层台阶)时我们能够很容易地得出结果。因此对于当前的青蛙而言,它能够选择的就是跳一级或跳二级,剩下的台阶有多少种跳法交给子过程来解决: + +```java +public int JumpFloor(int target) { + //input check + if(target <= 0){ + return 0; + } + //base case + if(target == 1){ + return 1; + } + if(target == 2){ + return 2; + } + return JumpFloor(target - 1) + JumpFloor(target - 2); +} +``` + +你会发现这其实就是一个斐波那契数列,只不过是从`f(1) = 1,f(2) = 2`开始的斐波那契数列罢了。自然你也应该能够写出动态规划版本。 + +#### 进阶问题 + +一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 + +#### 解析 + +##### 递归版本 + +本质上还是分解,只不过上一个是分解成两步,而这个是分解成n步: + +```java +public int JumpFloorII(int target) { + if(target <= 0){ + return 0; + } + //base case,当target=0时表示某个分解分支跳完了所有台阶,这个分支就是一种跳法 + if(target == 0){ + return 1; + } + + //本过程要收集的跳法的总数 + int res = 0; + for(int i = 1 ; i <= target ; i++){ + //本次选择,选择跳i阶台阶,剩下的台阶交给子过程,每个选择就代表一个分解分支 + res += JumpFloorII(target - i); + } + return res; +} +``` + +##### 动态规划 + +这个动态规划就有一点难度了,**首先我们要确定缓存目标**,斐波那契数列中由于`f(n)`只依赖于`f(n-1)`和`f(n-2)`因此我们仅用两个缓存变量实现了动态规划,但是这里`f(n)`依赖的是`f(0),f(1),f(2),...,f(n-1)`,因此我们需要通过长度量级为`n`的表缓存前`n`个状态(`int arr[] = new int[target + 1]`,`arr[target]`表示`f(n)`)。**然后根据递归版本(通常是`base case`)确定哪些状态的值是可以直接确定的**,比如由`if(target == 0){ return 1 }`可知`arr[0] = 1`,从`f(N = 1)`开始的所有状态都需要依赖之前(`f(n < N)`)的所有状态: + +```java +int res = 0; +for(int i = 1 ; i <= target ; i++){ + res += JumpFloorII(target - i); +} +return res +``` + +因此我们可以据此自下而上计算出每个子状态的值: + +```java +public int JumpFloorII(int target) { + if(target <= 0){ + return 0; + } + + int arr[] = new int[target + 1]; + arr[0] = 1; + for(int i = 1 ; i < arr.length ; i++){ + for(int j = 0 ; j < i ; j++){ + arr[i] += arr[j]; + } + } + + return arr[target]; +} +``` + +但这仍不是最优解,因为观察循环体你会发现,每次`f(n)`的计算都要从`f(0)`累加到`f(n-1)`,我们完全可以将这个累加值缓存起来`preSum`,每计算出一次`f(N)`之后都将缓存更新为`preSum += f(N)`。如此得到最优解: + +```java +public int JumpFloorII(int target) { + if(target <= 0){ + return 0; + } + + int arr[] = new int[target + 1]; + arr[0] = 1; + int preSum = arr[0]; + for(int i = 1 ; i < arr.length ; i++){ + arr[i] = preSum; + preSum += arr[i]; + } + + return arr[target]; +} +``` + +### 矩形覆盖 + +#### 题目描述 + +我们可以用`2*1`的小矩形横着或者竖着去覆盖更大的矩形。请问用n个`2*1`的小矩形无重叠地覆盖一个`2*n`的大矩形,总共有多少种方法? + +```java +public int RectCover(int target) { + +} +``` + + + +#### 解析 + +##### 递归版本 + +有了之前的历练,我们能很快的写出递归版本:先竖着放一个或者先横着放两个,剩下的交给递归处理: + +```java +//target 大矩形的边长,也是剩余小矩形的个数 +public int RectCover(int target) { + if(target <= 0){ + return 0; + } + if(target == 1 || target == 2){ + return target; + } + return RectCover(target - 1) + RectCover(target - 2); +} +``` + +##### 动态规划 + +这仍然是个以`f(1)=1,f(2)=2`开头的斐波那契数列: + +```java +//target 大矩形的边长,也是剩余小矩形的个数 +public int RectCover(int target) { + if(target <= 0){ + return 0; + } + if(target == 1 || target == 2){ + return target; + } + //n_1->f(n-1), n_2->f(n-2),从f(N=3)开始算起 + int n_1 = 2, n_2 = 1, N = 3, res = 0; + while(N++ <= target){ + res = n_1 + n_2; + n_2 = n_1; + n_1 = res; + } + + return res; +} +``` + +### 二进制中1的个数 + +#### 题目描述 + +输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。 + +```java +public int NumberOf1(int n) { + +} +``` + + + +#### 解析 + +题目已经给我们降低了难度:负数用补码(取反加1)表示表明输入的参数为均为正数,我们只需统计其二进制表示中1的个数、运算时只考虑无符号移位即可。 + +典型的判断某个二进制位上是否为1的方法是将该二进制数右移至该二进制位为最低位然后与1相与`&`,由于1的二进制表示中只有最低位为1其余位均为0,因此相与后的结果与该二进制位上的数相同。据此不难写出如下代码: + +```java +public int NumberOf1(int n) { + int count = 0; + for(int i = 0 ; i < 32 ; i++){ + count += ((n >> i) & 1); + } + return count; +} +``` + +当然了,还有一种比较秀的解法就是利用`n = n & (n - 1)`将`n`的二进制位中为1的最低位置为0(只要`n`不为0就说明含有二进位制为1的位,如此这样的操作能做多少次就说明有多少个二进制位为1的位): + +```java +public int NumberOf1(int n) { + int count = 0; + while(n != 0){ + count++; + n &= (n - 1); + } + return count; +} +``` + +### 数值的整数次方 + +#### 题目描述 + +给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。 + +```java +public double Power(double base, int exponent) { + +} +``` + + + +#### 解析 + +这是一道充满危险色彩的题,求职者可能会内心窃喜不假思索的写出如下代码: + +```java +public double Power(double base, int exponent) { + double res = 1; + for(int i = 1 ; i <= exponent ; i++){ + res *= base; + } + return res; +} +``` + +但是你有没有想过底数`base`和幂`exponent`都是可正、可负、可为0的。如果幂为负数,那么底数就不能为0,否则应该抛出算术异常: + +```java +//是否是负数 +boolean minus = false; +//如果存在分母 +if(exponent < 0){ + minus = true; + exponent = -exponent; + if(base == 0){ + throw new ArithmeticException("/ by zero"); + } +} +``` + +如果幂为0,那么根据任何不为0的数的0次方为1,0的0次方未定义,应该有如下判断: + +```java +//如果指数为0 +if(exponent == 0){ + if(base != 0){ + return 1; + }else{ + throw new ArithmeticException("0^0 is undefined"); + } +} +``` + +剩下的就是计算乘方结果,但是不要忘了如果幂为负需要将结果取倒数: + +```java +//指数不为0且分母也不为0,正常计算并返回整数或分数 +double res = 1; +for(int i = 1 ; i <= exponent ; i++){ + res *= base; +} + +if(minus){ + return 1/res; +}else{ + return res; +} +``` + +也许你还可以锦上添花为幂乘方的计算引入二分计算(当幂为偶数时`2^n = 2^(n/2) * 2^(n/2)`): + +```java +public double binaryPower(double base, int exp){ + if(exp == 1){ + return base; + } + double res = 1; + res *= (binaryPower(base, exp/2) * binaryPower(base, exp/2)); + return exp % 2 == 0 ? res : res * base; +} +``` + +### 调整数组顺序使奇数位于偶数前面 + +#### 题目描述 + +输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的**相对位置不变**。 + +```java +public void reOrderArray(int [] arr) { + +} +``` + + + +#### 解析 + +读题之后发现这个跟快排的`partition`思路很像,都是选取一个比较基准将数组分成两部分,当然你也可以以`arr[i] % 2 == 0`为基准将奇数放前半部分,将偶数放有半部分,但是虽然只需`O(n)`的时间复杂度但不能保证调整后奇数之间、偶数之间的相对位置: + +```java +public void reOrderArray(int [] arr) { + if(arr == null || arr.length == 0){ + return; + } + + int odd = -1; + for(int i = 0 ; i < arr.length ; i++){ + if(arr[i] % 2 == 1){ + swap(arr, ++odd, i); + } + } +} + +public void swap(int[] arr, int i, int j){ + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; +} +``` + +涉及到排序稳定性,我们自然能够想到插入排序,从数组的第二个元素开始向后依次确定每个元素应处的位置,确定的逻辑是:将该数与前一个数比较,如果比前一个数小则与前一个数交换位置并在交换位置后继续与前一个数比较直到前一个数小于等于该数或者已达数组首部停止。 + +此题不过是将比较的逻辑由数值的大小改为:当前的数是否是奇数并且前一个数是偶数,是则递归向前交换位置。代码示例如下: + +```java +public void reOrderArray(int [] arr) { + if(arr == null || arr.length == 0){ + return; + } + + int odd = -1; + for(int i = 1 ; i < arr.length ; i++){ + for(int j = i ; j >= 1 ; j--){ + if(arr[j] % 2 == 1 && arr[j - 1] % 2 == 0){ + swap(arr, j, j - 1); + } + } + } +} +``` + +### 链表中倒数第K个结点 + +#### 题目描述 + +输入一个链表,输出该链表中倒数第k个结点。 + +```java +public ListNode FindKthToTail(ListNode head,int k) { + +} +``` + + + +#### 解析 + +**倒数**,这又是一个从尾到头的遍历逻辑,而链表对从尾到头遍历是敏感的,前面我们有通过压栈/递归、反转链表的方式实现这个遍历逻辑,自然对于此题同样适用,但是那样未免太麻烦了,我们可以通过两个间距为(k-1)个结点的链表指针来达到此目的。 + +```java +public ListNode FindKthToTail(ListNode head,int k) { + //input check + if(head == null || k <= 0){ + return null; + } + ListNode tmp = new ListNode(0); + tmp.next = head; + ListNode p1 = tmp, p2 = tmp; + while(k > 0 && p1.next != null){ + p1 = p1.next; + k--; + } + //length < k + if(k != 0){ + return null; + } + while(p1 != null){ + p1 = p1.next; + p2 = p2.next; + } + + tmp = null; //help gc + + return p2; +} +``` + +这里使用了一个技巧,就是创建一个临时结点`tmp`作为两个指针的初始指向,以模拟`p1`先走`k`步之后,`p2`才开始走,没走时停留在初始位置的逻辑,有利于帮我们梳理指针在对应位置上的意义,这样当`p1`走到头时(`p1=null`),`p2`就是倒数第`k`个结点。 + +这里还有一个坑就是,笔者层试图为了简化代码将上述的`9 ~ 12`行写成如下偷懒模式而导致排错许久: + +```java +while(k-- > 0 && p1.next != null){ + p1 = p1.next; +} +``` + +原因是将`k--`写在`while()`中,无论判断是否通过都会执行`k = k - 1`,因此代码总是会在`if(k != 0)`处返回`null`,希望读者不要和笔者一样粗心。 + +> 总结:当遇到复杂的指针操作时,我们不妨试图多引入几个指针或者临时结点,以方便梳理我们的思路,加强代码的逻辑化,这些空间复杂度`O(1)`的操作通常也不会影响性能。 + +### 合并两个排序的链表 + +#### 题目描述 + +输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。 + +```java +public ListNode Merge(ListNode list1,ListNode list2) { + +} +``` + + + +#### 解析 + +![image](https://ws3.sinaimg.cn/large/006zweohgy1fzbx9j54uuj30jg0ak3yz.jpg) + +```java +public ListNode Merge(ListNode list1,ListNode list2) { + if(list1 == null || list2 == null){ + return list1 == null ? list2 : list1; + } + ListNode newHead = list1.val < list2.val ? list1 : list2; + ListNode p1 = (newHead == list1) ? list1.next : list1; + ListNode p2 = (newHead == list2) ? list2.next : list2; + ListNode p = newHead; + while(p1 != null && p2 != null){ + if(p1.val <= p2.val){ + p.next = p1; + p1 = p1.next; + }else{ + p.next = p2; + p2 = p2.next; + } + p = p.next; + } + + while(p1 != null){ + p.next = p1; + p = p.next; + p1 = p1.next; + } + while(p2 != null){ + p.next = p2; + p = p.next; + p2 = p2.next; + } + + return newHead; +} +``` + +### 树的子结构 + +#### 题目描述 + +输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构) + +```java +/** +public class TreeNode { + int val = 0; + TreeNode left = null; + TreeNode right = null; + + public TreeNode(int val) { + this.val = val; + + } + +}*/ +public boolean HasSubtree(TreeNode root1,TreeNode root2) { + if(root1 == null || root2 == null){ + return false; + } + + return process(root1, root2); +} +``` + + + +#### 解析 + +这是一道典型的分解求解的复杂问题。典型的二叉树分解:遍历头结点、遍历左子树、遍历右子树。首先按照`root1`和`root2`的值是否相等划分为两种情况: + +1. 两个头结点的值相等,并且`root2.left`也是`roo1.left`的子结构(递归)、`root2.right`也是`root1.right`的子结构(递归),那么可返回`true`。 +2. 否则,要看只有当`root2`为`root1.left`的子结构或者`root2`为`root1.right`的子结构时,才能返回`true` + +据上述两点很容易得出如下递归逻辑: + +```java +if(root1.val == root2.val){ + if(process(root1.left, root2.left) && process(root1.right, root2.right)){ + return true; + } +} + +return process(root1.left, root2) || process(root1.right, root2); +``` + +接下来确定递归的终止条件,如果某个子过程`root2=null`那么说明在自上而下的比较过程中`root2`的结点已被罗列比较完了,这时无论`root1`是否为`null`,该子过程都应该返回`true`: + +![image](https://ws4.sinaimg.cn/large/006zweohgy1fzbyis1e3oj30dg04qaa5.jpg) + +```java +if(root2 == null){ + return true; +} +``` + +但是如果`root2 != null`而`root1 = null`,则应返回`false` + +![image](https://wx3.sinaimg.cn/large/006zweohgy1fzbym9fv0bj30bv05974e.jpg) + +```java +if(root1 == null && root2 != null){ + return false; +} +``` + +完整代码如下: + +```java +public class Solution { + public boolean HasSubtree(TreeNode root1,TreeNode root2) { + if(root1 == null || root2 == null){ + return false; + } + + return process(root1, root2); + } + + public boolean process(TreeNode root1, TreeNode root2){ + if(root2 == null){ + return true; + } + if(root1 == null && root2 != null){ + return false; + } + + if(root1.val == root2.val){ + if(process(root1.left, root2.left) && process(root1.right, root2.right)){ + return true; + } + } + + return process(root1.left, root2) || process(root1.right, root2); + } +} +``` + +### 二叉树的镜像 + +#### 题目描述 + +操作给定的二叉树,将其变换为源二叉树的镜像。 + +![image](https://ws1.sinaimg.cn/large/006zweohgy1fzbyup8oq3j306b08kjra.jpg) + +```java +public void Mirror(TreeNode root) { + +} +``` + + + +#### 解析 + +由图可知获取二叉树的镜像就是将原树的每个结点的左右孩子交换一下位置(这个规律一定要会找),也就是说我们只需遍历每个结点并交换`left,right`的引用指向就可以了,而我们有成熟的先序遍历: + +```java +public void Mirror(TreeNode root) { + if(root == null){ + return; + } + + TreeNode tmp = root.left; + root.left = root.right; + root.right = tmp; + Mirror(root.left); + Mirror(root.right); +} +``` + +### 顺时针打印矩阵 + +#### 题目描述 + +输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10. + +```java +public ArrayList printMatrix(int [][] matrix) { + +} +``` + + + +#### 解析 + +![image](https://wx3.sinaimg.cn/large/006zweohgy1fzbzo7qyu0j30gr093q3a.jpg) + +只要分析清楚了打印思路(左上角和右下角即可确定一条打印轨迹)后,此题主要考查条件控制的把握。只要给我一个左上角的点`(i,j)`和右下角的点`(m,n)`,就可以将这一圈的打印分解为四步: + +![image](https://ws2.sinaimg.cn/large/006zweohgy1fzc01b7bpij309107hweh.jpg) + +但是如果左上角和右下角的点在一行或一列上那就没必要分解,直接打印改行或该列即可,打印的逻辑如下: + +```java +public void printEdge(int[][] matrix, int i, int j, int m, int n, ArrayList res){ + if(i == m && j == n){ + res.add(matrix[i][j]); + return; + } + + if(i == m || j == n){ + //only one while will be execute + while(i < m){ + res.add(matrix[i++][j]); + } + while(j < n){ + res.add(matrix[i][j++]); + } + res.add(matrix[m][n]); + return; + } + + int p = i, q = j; + while(q < n){ + res.add(matrix[p][q++]); + } + //q == n + while(p < m){ + res.add(matrix[p++][q]); + } + //p == m + while(q > j){ + res.add(matrix[p][q--]); + } + //q == j + while(p > i){ + res.add(matrix[p--][q]); + } + //p == i +} +``` + +接着我们将每个圈的左上角和右下角传入该函数即可: + +```java +public ArrayList printMatrix(int [][] matrix) { + ArrayList res = new ArrayList(); + if(matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0){ + return res; + } + int i = 0, j = 0, m = matrix.length - 1, n = matrix[0].length - 1; + while(i <= m && j <= n){ + printEdge(matrix, i++, j++, m--, n--, res); + } + return res; +} +``` + +### 包含min函数的栈 + +#### 题目描述 + +定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。 + +```java +public class Solution { + + + public void push(int node) { + + } + + public void pop() { + + } + + public int top() { + + } + + public int min() { + + } +} +``` + + + +#### 解析 + +最直接的思路是使用一个变量保存栈中现有元素的最小值,但这只对只存不取的栈有效,当弹出的值不是最小值时还没什么影响,但当弹出最小值后我们就无法获取当前栈中的最小值。解决思路是使用一个最小值栈,栈顶总是保存当前栈中的最小值,每次数据栈存入数据时最小值栈就要相应的将存入后的最小值压入栈顶: + +```java +private Stack dataStack = new Stack(); +private Stack minStack = new Stack(); + +public void push(int node) { + dataStack.push(node); + if(!minStack.empty() && minStack.peek() < node){ + minStack.push(minStack.peek()); + }else{ + minStack.push(node); + } +} + +public void pop() { + if(!dataStack.empty()){ + dataStack.pop(); + minStack.pop(); + } +} + +public int top() { + if(!dataStack.empty()){ + return dataStack.peek(); + } + throw new IllegalStateException("stack is empty"); +} + +public int min() { + if(!dataStack.empty()){ + return minStack.peek(); + } + throw new IllegalStateException("stack is empty"); +} +``` + +### 栈的压入、弹出序列 + +#### 题目描述 + +输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的**所有数字均不相等**。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的**长度是相等**的) + +```java +public boolean IsPopOrder(int [] arr1,int [] arr2) { + +} +``` + + + +#### 解析 + +可以使用两个指针`i,j`,初始时`i`指向压入序列的第一个,`j`指向弹出序列的第一个,试图将压入序列按照顺序压入栈中: + +1. 如果`arr1[i] != arr2[j]`,那么将`arr1[i]`压入栈中并后移`i`(表示`arr1[i]`还没到该它弹出的时刻) +2. 如果某次后移`i`之后发现`arr1[i] == arr2[j]`,那么说明此刻的`arr1[i]`被压入后应该被立即弹出才会产生给定的弹出序列,于是不压入`arr1[i]`(表示压入并弹出了)并后移`i`,`j`也要后移(表示弹出序列的`arr2[j]`记录已产生,接着产生或许的弹出记录即可)。 +3. 因为步骤2和3都会后移`i`,因此循环的终止条件是`i`到达`arr1.length`,此时若栈中还有元素,那么从栈顶到栈底形成的序列必须与`arr2`中`j`之后的序列相同才能返回`true`。 + +```java +public boolean IsPopOrder(int [] arr1,int [] arr2) { + //input check + if(arr1 == null || arr2 == null || arr1.length != arr2.length || arr1.length == 0){ + return false; + } + Stack stack = new Stack(); + int length = arr1.length; + int i = 0, j = 0; + while(i < length && j < length){ + if(arr1[i] != arr2[j]){ + stack.push(arr1[i++]); + }else{ + i++; + j++; + } + } + + while(j < length){ + if(arr2[j] != stack.peek()){ + return false; + }else{ + stack.pop(); + j++; + } + } + + return stack.empty() && j == length; +} +``` + +### 从上往下打印二叉树 + +#### 题目描述 + +从上往下打印出二叉树的每个节点,同层节点从左至右打印。 + +```java +public ArrayList PrintFromTopToBottom(TreeNode root) { + +} +``` + + + +#### 解析 + +使用一个队列来保存当前遍历结点的孩子结点,首先将根节点加入队列中,然后进行队列非空循环: + +1. 从队列头取出一个结点,将该结点的值打印 +2. 如果取出的结点左孩子不空,则将其左孩子放入队列尾部 +3. 如果取出的结点右孩子不空,则将其右孩子放入队列尾部 + +```java +public ArrayList PrintFromTopToBottom(TreeNode root) { + ArrayList res = new ArrayList(); + if(root == null){ + return res; + } + LinkedList queue = new LinkedList(); + queue.addLast(root); + while(queue.size() > 0){ + TreeNode node = queue.pollFirst(); + res.add(node.val); + if(node.left != null){ + queue.addLast(node.left); + } + if(node.right != null){ + queue.addLast(node.right); + } + } + + return res; +} +``` + +### 二叉搜索树的后序遍历序列 + +#### 题目描述 + +输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。 + +```java +public boolean VerifySquenceOfBST(int [] sequence) { + +} +``` + + + +#### 解析 + +对于二叉树的后序序列,我们能够确定最后一个数就是根结点,还能确定的是前一半部分是左子树的后序序列,后一部分是右子树的后序序列。 + +遇到这种复杂问题,我们仍能采用三步走战略(根结点、左子树、右子树): + +1. 如果当前根结点的左子树**是BST**且其右子树也是BST,那么才可能是BST +2. 在1的条件下,如果左子树的**最大值**小于根结点且右子树的**最小值**大于根结点,那么这棵树就是BST + +据此我们需要定义一个递归体,该递归体需要收集的信息如下:下层需要向我返回其最大值、最小值、以及是否是BST + +```java +class Info{ + boolean isBST; + int max; + int min; + Info(boolean isBST, int max, int min){ + this.isBST = isBST; + this.max = max; + this.min = min; + } +} +``` + +递归体的定义如下: + +```java +public Info process(int[] arr, int start, int end){ + if(start < 0 || end > arr.length - 1 || start > end){ + throw new IllegalArgumentException("invalid input"); + } + //base case : only one node + if(start == end){ + return new Info(true, arr[end], arr[end]); + } + + int root = arr[end]; + Info left, right; + //not exist left child + if(arr[start] > root){ + right = process(arr, start, end - 1); + return new Info(root < right.min && right.isBST, + Math.max(root, right.max), Math.min(root, right.min)); + } + //not exist right child + if(arr[end - 1] < root){ + left = process(arr, start, end - 1); + return new Info(root > left.max && left.isBST, + Math.max(root, left.max), Math.min(root, left.min)); + } + + int l = 0, r = end - 1; + while(r > l && r - l != 1){ + int mid = l + ((r - l) >> 1); + if(arr[mid] > root){ + r = mid; + }else{ + l = mid; + } + } + left = process(arr, start, l); + right = process(arr, r, end - 1); + return new Info(left.isBST && right.isBST && root > left.max && root < right.min, + right.max, left.min); +} +``` + +> 总结:二叉树相关的信息收集问题分步走: +> +> 1. 分析当前状态需要收集的信息 +> 2. 根据下层传来的信息加工出当前状态的信息 +> 3. 确定递归终止条件 + +### 二叉树中和为某一值的路径 + +#### 题目描述 + +输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下**一直到叶结点**所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前) + +```java +public ArrayList> FindPath(TreeNode root,int target) { + +} +``` + + + +#### 解析 + +审题可知,我们需要有一个自上而下从根结点到每个叶子结点的遍历思路,而先序遍历刚好可以拿来用,我们只需在来到当前结点时将当前结点值加入到栈中,在离开当前结点时再将栈中保存的当前结点的值弹出即可使用栈模拟保存自上而下经过的结点,从而实现在来到每个叶子结点时只需判断栈中数值之和是否为`target`即可。 + +```java +public ArrayList> FindPath(TreeNode root,int target) { + ArrayList> res = new ArrayList(); + if(root == null){ + return res; + } + Stack stack = new Stack(); + preOrder(root, stack, 0, target, res); + return res; +} + +public void preOrder(TreeNode root, Stack stack, int sum, int target, + ArrayList> res){ + if(root == null){ + return; + } + + stack.push(root.val); + sum += root.val; + //leaf node + if(root.left == null && root.right == null && sum == target){ + ArrayList one = new ArrayList(); + one.addAll(stack); + res.add(one); + } + + preOrder(root.left, stack, sum, target, res); + preOrder(root.right, stack, sum, target, res); + + sum -= stack.pop(); +} +``` + +### 复杂链表的复制 + +#### 题目描述 + +输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空) + +```java +/* +public class RandomListNode { + int label; + RandomListNode next = null; + RandomListNode random = null; + + RandomListNode(int label) { + this.label = label; + } +} +*/ +public class Solution { + public RandomListNode Clone(RandomListNode pHead) + { + + } +} +``` + +#### 解析 + +此题主要的难点在于`random`指针的处理。 + +##### 方法一:使用哈希表,额外空间O(n) + +可以将链表中的结点都复制一份,用一个哈希表来保存,`key`是源结点,`value`就是副本结点,然后遍历`key`取出每个对应的`value`将副本结点的`next`指针和`random`指针设置好: + +```java +public RandomListNode Clone(RandomListNode pHead){ + if(pHead == null){ + return null; + } + HashMap map = new HashMap(); + RandomListNode p = pHead; + //copy + while(p != null){ + RandomListNode cp = new RandomListNode(p.label); + map.put(p, cp); + p = p.next; + } + //link + p = pHead; + while(p != null){ + RandomListNode cp = map.get(p); + cp.next = (p.next == null) ? null : map.get(p.next); + cp.random = (p.random == null) ? null : map.get(p.random); + p = p.next; + } + + return map.get(pHead); +} +``` + +##### 方法二:追加结点,额外空间O(1) + +首先将每个结点复制一份并插入到对应结点之后,然后遍历链表将副本结点的`random`指针设置好,最后将源结点和副本结点分离成两个链表 + +```java +public RandomListNode Clone(RandomListNode pHead){ + if(pHead == null){ + return null; + } + + RandomListNode p = pHead; + while(p != null){ + RandomListNode cp = new RandomListNode(p.label); + cp.next = p.next; + p.next = cp; + p = p.next.next; + } + + //more than two node + //link random pointer + p = pHead; + RandomListNode cp; + while(p != null){ + cp = p.next; + cp.random = (p.random == null) ? null : p.random.next; + p = p.next.next; + } + + //split source and copy + p = pHead; + RandomListNode newHead = p.next; + //p != null -> p.next != null + while(p != null){ + cp = p.next; + p.next = p.next.next; + p = p.next; + cp.next = (p == null) ? null : p.next; + } + + return newHead; +} +``` + +### 二叉搜索树与双向链表 + +#### 题目描述 + +输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 + +```java +public TreeNode Convert(TreeNode root) { +} +``` + +#### 解析 + +典型的二叉树分解问题,我们可以定义一个黑盒`transform`,它的目的是将二叉树转换成双向链表,那么对于一个当前结点`root`,首先将其前驱结点(BST中前驱结点指中序序列的前一个数值,也就是当前结点的左子树上最右的结点,如果左子树为空则没有前驱结点)和后继结点(当前结点的右子树上的最左结点,如果右子树为空则没有后继结点),然后使用黑盒`transform`将左子树和右子树转换成双向链表,最后将当前结点和左子树形成的链表链起来(通过之前保存的前驱结点)和右子树形成的链表链起来(通过之前保存的后继结点),整棵树的转换完毕。 + +```java +public TreeNode Convert(TreeNode root) { + if(root == null){ + return null; + } + + //head is the most left node + TreeNode head = root; + while(head.left != null){ + head = head.left; + } + transform(root); + return head; +} + +//transform a tree to a double-link list +public void transform(TreeNode root){ + if(root == null){ + return; + } + TreeNode pre = root.left, next = root.right; + while(pre != null && pre.right != null){ + pre = pre.right; + } + while(next != null && next.left != null){ + next = next.left; + } + + transform(root.left); + transform(root.right); + //asume the left and right has transformed and what's remaining is link the root + root.left = pre; + if(pre != null){ + pre.right = root; + } + root.right = next; + if(next != null){ + next.left = root; + } +} +``` + +### 字符串全排列 + +#### 题目描述 + +输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。 + +#### 解析 + +定义一个递归体`generate(char[] arr, int index, TreeSet res)`,其中`char[] arr`和`index`组合表示上层状态给当前状态传递的信息,即`arr`中`0 ~ index-1`是已生成好的串,现在你(当前状态)要确定`index`位置上应该放什么字符(你可以从`index ~ arr.length - 1`上任选一个字符),然后将`index + 1`应该放什么字符递归交给子过程处理,当某个状态要确定`arr.length`上应该放什么字符时说明`0 ~ arr.length-1`位置上的字符已经生成好了,因此递归终止,将生成好的字符串记录下来(这里由于要求不能重复且按字典序排列,因此我们可以使用JDK中红黑树的实现`TreeSet`来做容器) + +```java +public ArrayList Permutation(String str) { + ArrayList res = new ArrayList(); + if(str == null || str.length() == 0){ + return res; + } + TreeSet set = new TreeSet(); + generate(str.toCharArray(), 0, set); + res.addAll(set); + return res; +} + +public void generate(char[] arr, int index, TreeSet res){ + if(index == arr.length){ + res.add(new String(arr)); + } + for(int i = index ; i < arr.length ; i++){ + swap(arr, index, i); + generate(arr, index + 1, res); + swap(arr, index, i); + } +} + +public void swap(char[] arr, int i, int j){ + if(arr == null || arr.length == 0 || i < 0 || j > arr.length - 1){ + return; + } + char tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; +} +``` + +> 注意:上述代码的第`19`行有个坑,笔者曾因忘记写第19行而排错许久,由于你任选一个`index ~ arr.length - 1`位置上的字符与`index`位置上的交换并将交换生成的结果交给了子过程(第`17,18`行),但你不应该影响后续选取其他字符放到`index`位置上而形成的结果,因此需要再交换回来(第`19`行) + +### 数组中出现次数超过一半的数 + +#### 题目描述 + +数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。 + +```java +public int MoreThanHalfNum_Solution(int [] arr) { +} +``` + +#### 解析 + +##### 方法一:基于partition查找数组中第k大的数 + +如果我们将数组排序,最快也要`O(nlogn)`,排序后的中位数自然就是出现次数超过长度一半的数。 + +我们知道快排的`partition`操作能够将数组按照一个基准划分成小于部分和大于等于部分并返回这个基准在数组中的下标,虽然一次`partition`并不能使数组整体有序,但是能够返回随机选择的数在`partition`之后的下标`index`,这个下标标识了它是第`index`大的数,这也意味着我们要求数组中第`k`大的数不一定要求数组整体有序。 + +于是我们在首次对整个数组`partition`之后将返回的`index`与`n/2`进行比较,并调整下一次`partition`的范围直到`index = n/2`为止我们就找到了。 + +这个时间复杂度需要使用`Master`公式计算(计算过程参见 http://www.zhenganwen.top/62859a9a.html#Master%E5%85%AC%E5%BC%8F),**使用`partition`查找数组中第k大的数时间复杂度为`O(n)`**,最后不要忘了验证一下`index = n/2`上的数出现的次数是否超过了长度的一半。 + +```java +public int MoreThanHalfNum_Solution(int [] arr) { + if(arr == null || arr.length == 0){ + return 0; + } + if(arr.length == 1){ + return arr[0]; + } + + int index = partition(arr, 0, arr.length - 1); + int half = arr.length >> 1;// 0 <= half <= arr.length - 1 + while(index != half){ + index = index > k ? partition(arr, 0, index - 1) : partition(arr, index + 1, arr.length - 1); + } + + int count = 0; + for(int i = 0 ; i < arr.length ; i++){ + count = (arr[i] == arr[index]) ? ++count : count; + } + + return (count > arr.length / 2) ? arr[index] : 0; +} + +public int partition(int[] arr, int start, int end){ + if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){ + throw new IllegalArgumentException(); + } + if(start == end){ + return end; + } + int random = start + (int)(Math.random() * (end - start + 1)); + swap(arr, random, end); + int small = start - 1; + for(int i = start ; i < end ; i++){ + if(arr[i] < arr[end]){ + swap(arr, ++small, i); + } + } + + swap(arr, ++small, end); + + return small; +} + +public void swap(int[] arr, int i, int j){ + int t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; +} +``` + +##### 方法二 + +1. 使用一个`target`记录一个数,并使用`count`记录它出现的次数 +2. 初始时`target = arr[0]`,`count = 1`,表示`arr[0]`出现了1次 +3. 从第二个元素开始遍历数组,如果遇到的数不等于`target`就将`count`减1,否则加1 +4. 如果遍历到某个数时,`count`为0了,那么就将`target`设置为该数,并将`count`置1,继续向后遍历 + +如果存在出现次数超过一半的数,那么必定是`target`最后一次被设置时的数。 + +```java +public int MoreThanHalfNum_Solution(int [] arr) { + if(arr == null || arr.length == 0){ + return 0; + } + //此题需要抓住出现次数超过数组长度的一半这个点来想 + //使用一个计数器,如果这个数出现一次就自增,否则自减,如果自减为0则更新被记录的数 + //如果存在出现次数大于一半的数,那么最后一次被记录的数就是所求之数 + int target = arr[0], count = 1; + for(int i = 1 ; i < arr.length ; i++){ + if(count == 0){ + target = arr[i]; + count = 1; + }else{ + count = (arr[i] == target) ? ++count : --count; + } + } + + if(count == 0){ + return 0; + } + + //不要忘了验证!!! + count = 0; + for(int i = 0 ; i < arr.length ; i++){ + count = (arr[i] == target) ? ++count : count; + } + + return (count > arr.length / 2) ? target : 0; +} +``` + +### 最小的k个数 + +#### 题目描述 + +输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。 + +```java +public ArrayList GetLeastNumbers_Solution(int [] arr, int k) { + +} +``` + +#### 解析 + +与上一题的求数组第k大的数如出一辙,如果某次`partition`之后你得到了第k大的数的下标,那么根据`partitin`规则该下标左边的数均比该下标上的数小,最小的k个数自然就是此时的`0~k-1`下标上的数 + +```java +public ArrayList GetLeastNumbers_Solution(int [] arr, int k) { + ArrayList res = new ArrayList(); + if(arr == null || arr.length == 0 || k <= 0 || k > arr.length){ + //throw new IllegalArgumentException(); + return res; + } + + int index = partition(arr, 0, arr.length - 1); + k = k - 1; + while(index != k){ + index = index > k ? partition(arr, 0, index - 1) : partition(arr, index + 1, arr.length - 1); + } + + for(int i = 0 ; i <= k ; i++){ + res.add(arr[i]); + } + + return res; +} + +public int partition(int[] arr, int start, int end){ + if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){ + throw new IllegalArgumentException(); + } + if(start == end){ + return end; + } + + int random = start + (int)(Math.random() * (end - start + 1)); + swap(arr, random, end); + int small = start - 1; + for(int i = start ; i < end ; i++){ + if(arr[i] < arr[end]){ + swap(arr, ++small, i); + } + } + + swap(arr, ++small, end); + return small; +} + +public void swap(int[] arr, int i, int j){ + int t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; +} +``` + +### 连续子数组的最大和 + +#### 题目描述 + +HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1) + +```java +public int FindGreatestSumOfSubArray(int[] arr) { + +} +``` + +#### 解析 + +##### 暴力解 + +暴力法是找出所有子数组,然后遍历求和,时间复杂度为`O(n^3)` + +```java +public int FindGreatestSumOfSubArray(int[] arr) { + if(arr == null || arr.length == 0){ + return 0; + } + int max = Integer.MIN_VALUE; + + //start + for(int i = 0 ; i < arr.length ; i++){ + //end + for(int j = i ; j < arr.length ; j++){ + //sum + int sum = 0; + for(int k = i ; k <= j ; k++){ + sum += arr[k]; + } + max = Math.max(max, sum); + } + } + + return max; +} +``` + +##### 最优解 + +使用一个`sum`记录累加和,初始时为0,遍历数组: + +1. 如果遍历到`i`时,发现`sum`小于0,那么丢弃这个累加和,将`sum`重置为`0` +2. 将当前元素累加到`sum`上,并更新最大和`maxSum` + +```java +public int FindGreatestSumOfSubArray(int[] arr) { + if(arr == null || arr.length == 0){ + return 0; + } + int sum = 0, max = Integer.MIN_VALUE; + for(int i = 0 ; i < arr.length ; i++){ + if(sum < 0){ + sum = 0; + } + sum += arr[i]; + max = Math.max(max, sum); + } + + return max; +} +``` + +### 整数中1出现的次数(从1到n整数中1出现的次数) + +#### 题目描述 + +求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。 + +#### 解析 + +##### 遍历一遍不就完了吗 + +当然,你可从1遍历到n,然后将当前被遍历的到的数中1出现的次数累加到结果中可以很容易地写出如下代码: + +```java +public int NumberOf1Between1AndN_Solution(int n) { + if(n < 1){ + return 0; + } + int res = 0; + for(int i = 1 ; i <= n ; i++){ + res += count(i); + } + return res; +} + +public int count(int n){ + int count = 0; + while(n != 0){ + //取个位 + count = (n % 10 == 1) ? ++count : count; + //去掉个位 + n /= 10; + } + return count; +} +``` + +但n多大就会循环多少次,这并不是面试官所期待的,这时我们就需要找规律看是否有捷径可走 + +##### 不用数我也知道 + +以`51234`这个数为例,我们可以先将`51234`划分成`1~1234`(去掉最高位)和`1235~51234`两部分来求解。下面先分析`1235~51234`这个区间的结果: + +1. 所有的数中,1在最高位(万位)出现的次数 + + 对于`1235~51234`,最高位为1时(即万位为1时)的数有`10000~19999`这10000个数,也就是说1在最高位(万位)出现的次数为10000,因此我们可以得出结论:如果最高位大于1,那么在最高位上1出现的次数为最高位对应的单位(本例中为一万次);但如果最高位为1,比如`1235~11234`,那么次数就为去掉最高位之后的数了,`11234`去掉最高位后是`1234`,即1在最高位上出现的次数为`1234` + +2. 所有的数中,1在非最高位上出现的次数 + + 我们可以进一步将`1235~51234`按照最高位的单位划分成4个区间(能划分成几个区间由最高位上的数决定,这里最高位为5,所以能划分5个大小为一万子区间): + + - `1235~11234` + - `11235~21234` + - `21235~31234` + - `31235~41234` + - `41235~51234` + + 而每个数不考虑万位(因为1在万位出现的总次数在步骤1中已统计好了),其余四位(个、十、百、千)取一位放1(比如千位),剩下的3位从`0~9`中任意选(`10 * 10 * 10`),那么仅统计1在千位上出现的次数之和就是:`5(子区间数) * 10 * 10 * 10`,还有百位、十位、个位,结果为:`4 * 10 * 10 * 10 * 5`。 + + 因此非高位上1出现的总次数的计算通式为:`(n-1) * 10^(n-2) * 十进制最高位上的数 `(其中`n`为十进制的总位数) + + 于是`1235 ~ 51234`之间所有的数的所有的位上1出现的次数的综合我们就计算出来了 + +剩下`1 ~ 1234`,你会发现这与`1 ~ 51234`的问题是一样的,因此可以做递归处理,即子过程也会将`1 ~ 1234`也分成`1 ~ 234`和`235 ~ 1234`两部分,并计算`235~1234`而将`1~234`又进行递归处理。 + +而递归的终止条件如下: + +1. 如果`1~n`中的`n`:`1 <= n <= 9`,那么就可以直接返回1了,因为只有数1出现了一次1 +2. 如果`n == 0`,比如将`10000`划分成的两部分是`0 ~ 0(10000去掉最高位后的结果)`和`1 ~ 10000`,那么就返回0 + +```java +public int NumberOf1Between1AndN_Solution(int n) { + if(n < 1){ + return 0; + } + return process(n); +} + +public int process(int n){ + if(n == 0){ + return 0; + } + if(n < 10 && n > 0){ + return 1; + } + int res = 0; + //得到十进制位数 + int bitCount = bitCount(n); + //十进制最高位上的数 + int highestBit = numOfBit(n, bitCount); + //1、统计最高位为1时,共有多少个数 + if(highestBit > 1){ + res += powerOf10(bitCount - 1); + }else{ + //highestBit == 1 + res += n - powerOf10(bitCount - 1) + 1; + } + //2、统计其它位为1的情况 + res += powerOf10(bitCount - 2) * (bitCount - 1) * highestBit; + //3、剩下的部分交给递归 + res += process(n % powerOf10(bitCount - 1)); + return res; +} + +//返回10的n次方 +public int powerOf10(int n){ + if(n == 0){ + return 1; + } + boolean minus = false; + if(n < 0){ + n = -n; + minus = true; + } + int res = 1; + for(int i = 1 ; i <= n ; i++){ + res *= 10; + } + return minus ? 1 / res : res; +} + +public int bitCount(int n){ + int count = 1; + while((n /= 10) != 0){ + count++; + } + return count; +} + +public int numOfBit(int n, int bit){ + while(bit-- > 1){ + n /= 10; + } + return n % 10; +} +``` + +笔者曾纠结,对于一个四位数,每个位上出现1时都统计了一遍会不会有重复,比如`11111`这个数在最高位为1时的`10000 ~ 19999`统计了一遍,在统计非最高位的其他位上为1时又统计了4次,总共被统计了5次,而这个数1出现的次数也确实是5次,因此没有重复。 + +### 把数组排成最小的数 + +#### 题目描述 + +输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。 + +#### 解析 + +这是一个贪心问题,你发现将数组按递增排序之后依次连接起来的结果并不是最优的结果,于是需要寻求贪心策略,对于这类最小数和最小字典序的问题而言,贪心策略是:如果`3`和`32`相连的结果大于`32`和`3`相连的结果,那么视作`3`比`32`大,最后我们需要按照按照这种策略将数组进行升序排序,以得到首尾相连之后的结果是最小数字(最小字典序)。 + +```java +public String PrintMinNumber(int [] numbers) { + if(numbers == null || numbers.length == 0){ + return ""; + } + List list = new ArrayList(); + for(int num : numbers){ + list.add(num); + } + Collections.sort(list, new MyComparator()); + StringBuilder res = new StringBuilder(""); + for(Integer integer : list){ + res.append(integer.toString()); + } + return res.toString(); +} + +class MyComparator implements Comparator{ + public int compare(Integer i1, Integer i2){ + String s1 = i1.toString() + i2.toString(); + String s2 = i2.toString() + i1.toString(); + return Integer.parseInt(s1) - Integer.parseInt(s2); + } +} +``` + +### 丑数 + +#### 题目描述 + +把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。 + +#### 解析 + +老实说,在《剑指offer》上看这道题的时候每太看懂,以至于第一遍在牛客网OJ这道题的时候都是背下来写上去的,直到这第二遍总结时才弄清整个思路,思路的核心就是第一个丑数是1(题目给的),此后的每一个丑数都是由之前的某个丑数与2或3或5的乘积得来 + +![image](https://wx4.sinaimg.cn/large/006zweohgy1fzdddzfdfnj30pm0d4jtb.jpg) + +```java +public int GetUglyNumber_Solution(int index) { + if(index < 1){ + //throw new IllegalArgumentException("index must bigger than one"); + return 0; + } + if(index == 1){ + return 1; + } + + int[] arr = new int[index]; + arr[0] = 1; + int indexOf2 = 0, indexOf3 = 0, indexOf5 = 0; + for(int i = 1 ; i < index ; i++){ + arr[i] = Math.min(arr[indexOf2] * 2, Math.min(arr[indexOf3] * 3, arr[indexOf5] * 5)); + indexOf2 = (arr[indexOf2] * 2 <= arr[i]) ? ++indexOf2 : indexOf2; + indexOf3 = (arr[indexOf3] * 3 <= arr[i]) ? ++indexOf3 : indexOf3; + indexOf5 = (arr[indexOf5] * 5 <= arr[i]) ? ++indexOf5 : indexOf5; + } + + return arr[index - 1]; +} +``` + +### 第一个只出现一次的字符 + +#### 题目描述 + +在一个字符串(0<=字符串长度<=10000,**全部由字母组成**)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写). + +#### 解析 + +可以从头遍历字符串,并使用一个表记录每个字符第一次出现的位置(初始时表中记录的位置均为-1),如果记录当前被遍历字符出现的位置时发现之前已经记录过了(通过查表,该字符的位置不是-1而是大于等于0的一个有效索引),那么当前字符不在答案的考虑范围内,通过将表中该字符的出现索引标记为`-2`来标识。 + +遍历一遍字符串并更新表之后,再遍历一遍字符串,如果发现某个字符在表中对应的记录是一个有效索引(大于等于0),那么该字符就是整个串中第一个只出现一次的字符。 + +由于题目标注字符串全都由字母组成,而字母可以使用`ASCII`码表示且`ASCII`范围为`0~255`,因此使用了一个长度为`256`的数组来实现这张表。用字母的`ASCII`值做索引,索引对应的值就是字母在字符串中第一次出现的位置(初始时为-1,第一次遇到时设置为出现的位置,重复遇到时置为-2)。 + +```java +public int FirstNotRepeatingChar(String str) { + if(str == null || str.length() == 0){ + return -1; + } + //全部由字母组成 + int[] arr = new int[256]; + for(int i = 0 ; i < arr.length ; i++){ + arr[i] = -1; + } + for(int i = 0 ; i < str.length() ; i++){ + int ascii = (int)str.charAt(i); + if(arr[ascii] == -1){ + //set index of first apearance + arr[ascii] = i; + }else if(arr[ascii] >= 0){ + //repeated apearance, don't care + arr[ascii] = -2; + } + //arr[ascii] == -2 -> do not care + } + + for(int i = 0 ; i < str.length() ; i++){ + int ascii = (int)str.charAt(i); + if(arr[ascii] >= 0){ + return arr[ascii]; + } + } + + return -1; +} +``` + +### 数组中的逆序对 + +#### 题目描述 + +在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007 + +```java +public int InversePairs(int [] arr) { + if(arr == null || arr.length <= 1){ + return 0; + } + return mergeSort(arr, 0, arr.length - 1).pairs; +} +``` + +#### 输入描述 + +1. 题目保证输入的数组中没有相同的数字 +2. 数据范围:对于%50的数据,size<=10^4;对于%75的数据,size<=10^5;对于%100的数据,size<=2*10^5 + + + +#### 解析 + +借助归并排序的流程,将归并流程中前一个数组的数比后一个数组的数小的情况记录下来。 + +归并的原始逻辑是根据输入的无序数组返回一个新建的排好序的数组: + +```java +public int[] mergeSort(int[] arr, int start, int end){ + if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){ + throw new IllegalArgumentException(); + } + if(start == end){ + return new int[]{ arr[end] }; + } + + int[] arr1 = mergeSort(arr, start, mid); + int[] arr2 = Info right = mergeSort(arr, mid + 1, end); + int[] copy = new int[arr1.length + arr2.length]; + int p1 = 0, p2 = 0, p = 0; + + while(p1 < arr1.length && p2 < arr2.length){ + if(arr1[p1] > arr2[p2]){ + copy[p++] = arr1[p1++]; + }else{ + copy[p++] = arr2[p2++]; + } + } + while(p1 < arr1.length){ + copy[p++] = arr1[p1++]; + } + while(p2 < arr2.length){ + copy[p++] = arr2[p2++]; + } + return copy; +} +``` + +而我们需要再此基础上对子状态收集的信息进行改造,假设左右两半部分分别有序了,那么进行`merge`的时候,不应是从前往后复制了,这样当`arr1[p1] > arr2[p2]`的时候并不知道`arr2`的`p2`后面还有多少元素是比`arr1[p1]`小的,要想一次比较就统计出`arr2`中所有比`arr1[p1]`小的数需要将`p1,p2`从`arr1,arr2`的尾往前遍历: + +![image](https://ws4.sinaimg.cn/large/006zweohgy1fzdg2nzuzkj30n006odg2.jpg) + +而将比较后较大的数移入辅助数组的逻辑还是一样。这样当前递归状态需要收集左半子数组和右半子数组的变成有序过程中记录的逆序对数和自己`merge`记录的逆序对数之和就是当前状态要返回的信息,并且`merge`后形成的有序辅助数组也要返回。 + +```java +public int InversePairs(int [] arr) { + if(arr == null || arr.length <= 1){ + return 0; + } + return mergeSort(arr, 0, arr.length - 1).pairs; +} + +class Info{ + int arr[]; + int pairs; + Info(int[] arr, int pairs){ + this.arr = arr; + this.pairs = pairs; + } +} + +public Info mergeSort(int[] arr, int start, int end){ + if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){ + throw new IllegalArgumentException(); + } + if(start == end){ + return new Info(new int[]{arr[end]}, 0); + } + + int pairs = 0; + int mid = start + ((end - start) >> 1); + Info left = mergeSort(arr, start, mid); + Info right = mergeSort(arr, mid + 1, end); + pairs += (left.pairs + right.pairs) % 1000000007; + + int[] arr1 = left.arr, arr2 = right.arr, copy = new int[arr1.length + arr2.length]; + int p1 = arr1.length - 1, p2 = arr2.length - 1, p = copy.length - 1; + + while(p1 >= 0 && p2 >= 0){ + if(arr1[p1] > arr2[p2]){ + pairs += (p2 + 1); + pairs %= 1000000007; + copy[p--] = arr1[p1--]; + }else{ + copy[p--] = arr2[p2--]; + } + } + + while(p1 >= 0){ + copy[p--] = arr1[p1--]; + } + while(p2 >= 0){ + copy[p--] = arr2[p2--]; + } + + return new Info(copy, pairs % 1000000007); +} +``` + +### 两个链表的第一个公共结点 + +#### 题目描述 + +输入两个链表,找出它们的第一个公共结点。 + +```java +public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + +} +``` + +#### 解析 + +首先我们要分析两个链表的组合状态,根据有环、无环相互组合只可能会出现如下几种情况: + +![image](https://ws4.sinaimg.cn/large/006zweohgy1fzdz1wxjy8j30pc0cmmzb.jpg) + +于是我们首先要判断两个链表是否有环,判断链表是否有环以及有环链表的入环结点在哪已有前人给我们总结好了经验: + +1. 使用一个快指针和一个慢指针同时从首节点出发,快指针一次走两步而慢指针一次走一步,如果两指针相遇则说明有环,否则无环 +2. 如果两指针相遇,先将快指针重新指向首节点,然后两指针均一次走一步,再次相遇时的结点就是入环结点 + +```java +public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + //若其中一个链表为空则不存在相交问题 + if(pHead1 == null || pHead2 == null){ + return null; + } + ListNode ringNode1 = ringNode(pHead1); + ListNode ringNode2 = ringNode(pHead2); + //如果一个有环,另一个无环 + if((ringNode1 == null && ringNode2 != null) || + (ringNode1 != null && ringNode2 == null)){ + return null; + } + //如果两者都无环,判断是否共用尾结点 + else if(ringNode1 == null && ringNode2 == null){ + return firstCommonNode(pHead1, pHead2, null); + } + //剩下的情况就是两者都有环了 + else{ + //如果入环结点相同,那么第一个相交的结点肯定在入环结点之前 + if(ringNode1 == ringNode2){ + return firstCommonNode(pHead1, pHead2, ringNode1); + } + //如果入环结点不同,看能否通过ringNode1的后继找到ringNode2 + else{ + ListNode p = ringNode1; + while(p.next != ringNode1){ + p = p.next; + if(p == ringNode2){ + break; + } + } + //如果能找到,那么第一个相交的结点既可以是ringNode1也可以是ringNode2 + return (p == ringNode2) ? ringNode1 : null; + } + } +} + +//查找两链表的第一个公共结点,如果两链表无环,则传入common=null,如果都有环且入环结点相同,那么传入common=入环结点 +public ListNode firstCommonNode(ListNode pHead1, ListNode pHead2, ListNode common){ + ListNode p1 = pHead1, p2 = pHead2; + int len1 = 1, len2 = 1, gap = 0; + while(p1.next != common){ + p1 = p1.next; + len1++; + } + while(p2.next != common){ + p2 = p2.next; + len2++; + } + //如果是两个无环链表,要判断一下是否有公共尾结点 + if(common == null && p1 != p2){ + return null; + } + gap = len1 > len2 ? len1 - len2 : len2 - len1; + //p1指向长链表,p2指向短链表 + p1 = len1 > len2 ? pHead1 : pHead2; + p2 = len1 > len2 ? pHead2 : pHead1; + while(gap-- > 0){ + p1 = p1.next; + } + while(p1 != p2){ + p1 = p1.next; + p2 = p2.next; + } + return p1; +} + +//判断链表是否有环,没有返回null,有则返回入环结点(整个链表是一个环时入环结点就是头结点) +public ListNode ringNode(ListNode head){ + if(head == null){ + return null; + } + ListNode p1 = head, p2 = head; + while(p1.next != null && p1.next.next != null){ + p1 = p1.next.next; + p2 = p2.next; + if(p1 == p2){ + break; + } + } + + if(p1.next == null || p1.next.next == null){ + return null; + } + + p1 = head; + while(p1 != p2){ + p1 = p1.next; + p2 = p2.next; + } + //可能整个链表就是一个环,这时入环结点就是头结点!!! + return p1 == p2 ? p1 : head; +} +``` + +### 数字在排序数组中出现的次数 + +#### 题目描述 + +统计一个数字在排序数组中出现的次数。 + +```java +public int GetNumberOfK(int [] array , int k) { + +} +``` + +#### 解析 + +我们可以分两步解决,先找出数值为k的连续序列的左边界,再找右边界。可以采用二分的方式,以查找左边界为例:如果`arr[mid]`小于`k`那么移动左指针,否则移动右指针(初始时左指针指向`-1`,而右指针指向尾元素`arr.length`),当两个指针相邻时,左指针及其左边的数均小于`k`而右指针及其右边的数均大于或等于`k`,因此此时右指针就是要查找的左边界,同样的方式可以求得右边界。 + +值得注意的是,笔者曾将左指针初始化为`0`而右指针初始化为`arr.length - 1`,这与指针指向的含义是相悖的,因为左指针指向的元素必须是小于`k`的,而我们并不能保证`arr[0]`一定小于`k`,同样的我们也不能保证`arr[arr.length - 1]`一定大于等于`k`。 + +还有一点就是如果数组中没有`k`这个算法是否依然会返回一个正确的值(0),这也是需要验证的。 + +```java +public int GetNumberOfK(int [] arr , int k) { + if(arr == null || arr.length == 0){ + return 0; + } + if(arr.length == 1){ + return (arr[0] == k) ? 1 : 0; + } + + int start, end, left, right; + for(start = -1, end = arr.length ; end > start && end - start != 1 ;){ + int mid = start + ((end - start) >> 1); + if(arr[mid] >= k){ + end = mid; + }else{ + start = mid; + } + } + left = end; + for(start = -1, end = arr.length; end > start && end - start != 1 ;){ + int mid = start + ((end - start) >> 1); + if(arr[mid] > k){ + end = mid; + }else{ + start = mid; + } + } + right = start; + return right - left + 1; +} +``` + +### 二叉树的深度 + +#### 题目描述 + +输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 + +```java +public int TreeDepth(TreeNode root) { +} +``` + +#### 解析 + +1. 将`TreeDepth`看做一个黑盒,假设利用这个黑盒收集到了左子树和右子树的深度,那么当前这棵树的深度就是前面两者的最大值加1 +2. `base case`,如果当前是一棵空树,那么深度为0 + +```java +public class Solution { + public int TreeDepth(TreeNode root) { + if(root == null){ + return 0; + } + return Math.max(TreeDepth(root.left), TreeDepth(root.right)) + 1; + } +} +``` + +### 平衡二叉树 + +#### 题目描述 + +输入一棵二叉树,判断该二叉树是否是平衡二叉树。 + +```java +public boolean IsBalanced_Solution(TreeNode root) { + +} +``` + +#### 解析 + +判断当前这棵树是否是平衡二叉所需要收集的信息: + +1. 左子树、右子树各自是平衡二叉树吗(需要收集子树是否是平衡二叉树) +2. 如果1成立,还需要收集左子树和右子树的高度,如果高度相差不超过1那么当前这棵树才是平衡二叉树(需要收集子树的高度) + +```java +class Info{ + boolean isBalanced; + int height; + Info(boolean isBalanced, int height){ + this.isBalanced = isBalanced; + this.height = height; + } +} +``` + +递归体的定义:(这里高度之差不超过1中的`left.height - right.height == 0`容易被忽略) + +```java +public boolean IsBalanced_Solution(TreeNode root) { + return process(root).isBalanced; +} + +public Info process(TreeNode root){ + if(root == null){ + return new Info(true, 0); + } + Info left = process(root.left); + Info right = process(root.right); + if(!left.isBalanced || !right.isBalanced){ + //如果左子树或右子树不是平衡二叉树,那么当前这棵树肯定也不是,树高度信息也就没用了 + return new Info(false, 0); + } + //高度之差不超过1 + if(left.height - right.height == 1 || left.height - right.height == -1 || + left.height - right.height == 0){ + return new Info(true, Math.max(left.height, right.height) + 1); + } + return new Info(false, 0); +} +``` + +### 数组中只出现一次的数字 + +#### 题目描述 + +一个整型数组里除了两个数字之外,其他的数字都出现了偶数次。请写程序找出这两个只出现一次的数字。 + +#### 解析 + +如果没有解过类似的题目,思路比较难打开。面试官可能会提醒你,如果是让你求一个整型数组里只有一个数只出现了一次而其它数出现了偶数次呢?你应该联想到: + +1. **偶数次相同的数异或的结果是0** +2. **任何数与0异或的结果是它本身** + +于是将数组从头到尾求异或和便可得知结果。那么对于此题,能否将数组分成这样的两部分呢:每个部分只有一个数出现了一次,其他的数都出现偶数次。 + +如果我们仍将整个数组从头到尾求异或和,那结果应该和这两个只出现一次的数的异或结果相同,目前我们所能依仗的也就是这个结果了,能否靠这个结果将数组分成想要的两部分? + +由于两个只出现一次的数(用A和B表示)异或结果`A ^ B`肯定不为0,那么`A ^ B`的二进制表示中肯定包含数值为1的bit位,而这个位上的1肯定是由A或B提供的,也就是说我们能根据**这个bit位上的数是否为1**来区分A和B,那剩下的数呢? + +由于剩下的数都出现偶数次,因此相同的数都会被分到一边(按照某个bit位上是否为1来分)。 + +```java +public void FindNumsAppearOnce(int [] arr,int num1[] , int num2[]) { + if(arr == null || arr.length <= 1){ + return; + } + int xorSum = 0; + for(int num : arr){ + xorSum ^= num; + } + //取xorSum二进制表示中低位为1的bit位,将其它的bit位 置0 + //比如:xorSum = 1100,那么 (1100 ^ 1011) & 1100 = 0100,只剩下一个为1的bit位 + xorSum = (xorSum ^ (xorSum - 1)) & xorSum; + + for(int num : arr){ + num1[0] = (num & xorSum) == 0 ? num1[0] ^ num : num1[0]; + num2[0] = (num & xorSum) != 0 ? num2[0] ^ num : num2[0]; + } +} +``` + +### 和为S的连续正数序列 + +#### 题目描述 + +小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck! + +```java +public ArrayList > FindContinuousSequence(int sum) { + +} +``` + +#### 输出描述 + +输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序 + +#### 解析 + +将`1 ~ (S / 2 + 1)`区间的数`n`依次加入到队列中(因为从`S/2 + 1`之后的任意两个正数之和都大于`S`): + +1. 将`n`加入到队列`queue`中并将队列元素之和`queueSum`更新,更新`queueSum`之后如果发现等于`sum`,那么将此时的队列快照加入到返回结果`res`中,并弹出队首元素(**保证下次入队操作时队列元素之和是小于sum的**) +2. 更新`queueSum`之后如果发现大于`sum`,那么循环弹出队首元素直到`queueSum <= Sum`,如果循环弹出之后发现`queueSum == sum`那么将队列快照加入到`res`中,并弹出队首元素(**保证下次入队操作时队列元素之和是小于sum的**);如果`queueSum < sum`那么入队下一个`n` + +于是有如下代码: + +```java +public ArrayList> FindContinuousSequence(int sum) { + ArrayList> res = new ArrayList(); + if(sum <= 1){ + return res; + } + LinkedList queue = new LinkedList(); + int n = 1, halfSum = (sum >> 1) + 1, queueSum = 0; + while(n <= halfSum){ + queue.addLast(n); + queueSum += n; + if(queueSum == sum){ + ArrayList one = new ArrayList(); + one.addAll(queue); + res.add(one); + queueSum -= queue.pollFirst(); + }else if(queueSum > sum){ + while(queueSum > sum){ + queueSum -= queue.pollFirst(); + } + if(queueSum == sum){ + ArrayList one = new ArrayList(); + one.addAll(queue); + res.add(one); + queueSum -= queue.pollFirst(); + } + } + n++; + } + + return res; +} +``` + +我们发现`11~15`和`20~24`行的代码是重复的,于是可以稍微优化一下: + +```java +public ArrayList> FindContinuousSequence(int sum) { + ArrayList> res = new ArrayList(); + if(sum <= 1){ + return res; + } + LinkedList queue = new LinkedList(); + int n = 1, halfSum = (sum >> 1) + 1, queueSum = 0; + while(n <= halfSum){ + queue.addLast(n); + queueSum += n; + if(queueSum > sum){ + while(queueSum > sum){ + queueSum -= queue.pollFirst(); + } + } + if(queueSum == sum){ + ArrayList one = new ArrayList(); + one.addAll(queue); + res.add(one); + queueSum -= queue.pollFirst(); + } + n++; + } + + return res; +} +``` + +### 和为S的两个数字 + +#### 题目描述 + +输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。 + +```java +public ArrayList FindNumbersWithSum(int [] arr,int sum) { + +} +``` + +#### 输出描述 + +对应每个测试案例,输出查找到的两个数,如果有多对,输出乘积最小的两个。 + +#### 解析 + +使用指针`l,r`,初始时`l`指向首元素,`r`指向尾元素,当两指针元素之和不等于`sum`且`r`指针在`l`指针右侧时循环: + +1. 如果两指针元素之和大于`sum`,那么将`r`指针左移,试图减小两指针之和 +2. 如果两指针元素之和小于`sum`,那么将`l`右移,试图增大两指针之和 +3. 如果两指针元素之和等于`sum`那么就可以返回了,或者`r`跑到了`l`的左边表名没有和`sum`的两个数,也可以返回了。 + +```java +public ArrayList FindNumbersWithSum(int [] arr,int sum) { + ArrayList res = new ArrayList(); + if(arr == null || arr.length <= 1 ){ + return res; + } + int l = 0, r = arr.length - 1; + while(arr[l] + arr[r] != sum && r > l){ + if(arr[l] + arr[r] > sum){ + r--; + }else{ + l++; + } + } + if(arr[l] + arr[r] == sum){ + res.add(arr[l]); + res.add(arr[r]); + } + return res; +} +``` + +### 旋转字符串 + +#### 题目描述 + +汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它! + +```java +public String LeftRotateString(String str,int n) { + +} +``` + +#### 解析 + +将开头的一段子串移到串尾:将开头的子串翻转一下、将剩余的子串翻转一下,最后将整个子串翻转一下。按理来说应该输入`char[] str`的,这样的话这种算法不会使用额外空间。 + +```java +public String LeftRotateString(String str,int n) { + if(str == null || str.length() == 0 || n <= 0){ + return str; + } + char[] arr = str.toCharArray(); + reverse(arr, 0, n - 1); + reverse(arr, n, arr.length - 1); + reverse(arr, 0, arr.length - 1); + return new String(arr); +} + +public void reverse(char[] str, int start, int end){ + if(str == null || str.length == 0 || start < 0 || end > str.length - 1 || start >= end){ + return; + } + for(int i = start, j = end ; j > i ; i++, j--){ + char tmp = str[i]; + str[i] = str[j]; + str[j] = tmp; + } +} +``` + +### 翻转单词顺序列 + +#### 题目描述 + +牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么? + +```java +public String LeftRotateString(String str,int n) { + +} +``` + +#### 解析 + +先将整个字符串翻转,最后按照标点符号或空格一次将句中的单词翻转。注意:由于最后一个单词后面没有空格,因此需要单独处理!!! + +```java +public String ReverseSentence(String str) { + if(str == null || str.length() <= 1){ + return str; + } + char[] arr = str.toCharArray(); + reverse(arr, 0, arr.length - 1); + int start = -1; + for(int i = 0 ; i < arr.length ; i++){ + if(arr[i] != ' '){ + //初始化start + start = (start == -1) ? i : start; + }else{ + //如果是空格,不用担心start>i-1,reverse会忽略它 + reverse(arr, start, i - 1); + start = i + 1; + } + } + //最后一个单词,这里比较容易忽略!!! + reverse(arr, start, arr.length - 1); + + return new String(arr); +} + +public void reverse(char[] str, int start, int end){ + if(str == null || str.length == 0 || start < 0 || end > str.length - 1 || start >= end){ + return ; + } + for(int i = start, j = end ; j > i ; i++, j--){ + char tmp = str[i]; + str[i] = str[j]; + str[j] = tmp; + } +} +``` + +### 扑克牌顺子 + +#### 题目描述 + +LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)...他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子.....LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。 + +#### 解析 + +先将数组排序(5个元素排序时间复杂O(1)),然后遍历数组统计王的数量和相邻非王牌之间的缺口数(需要用几个王来填)。还有一点值得注意:如果发现两种相同的非王牌,则不可能组成五张不同的顺子。 + +```java +public boolean isContinuous(int [] arr) { + if(arr == null || arr.length != 5){ + return false; + } + //5 numbers -> O(1) + Arrays.sort(arr); + int zeroCount = 0, slots = 0; + for(int i = 0 ; i < arr.length ; i++){ + //如果遇到两张相同的非王牌则不可能组成顺子,这点很容易忽略!!! + if(i > 0 && arr[i - 1] != 0){ + if(arr[i] == arr[i - 1]){ + return false; + }else{ + slots += arr[i] - arr[i - 1] - 1; + } + + } + zeroCount = (arr[i] == 0) ? ++zeroCount : zeroCount; + } + + return zeroCount >= slots; +} +``` + +### 孩子们的游戏(圆圈中剩下的数) + +#### 题目描述 + +每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1) + +#### 解析 + +1. 报数时,在报到`m-1`之前,可通过报数求得报数的结点编号: + + ![image](https://ws1.sinaimg.cn/large/006zweohgy1fze7t11d3mj30j309ewey.jpg) + +2. 在某个结点(小朋友)出列后的重新编号过程中,可通过新编号求结点的就编号 + + ![image](https://wx1.sinaimg.cn/large/006zweohgy1fze8z9c5dcj30o40eg0u7.jpg) + + 因此在某轮重新编号时,我们能在已知新编号`x`的情况下通过公式`y = (x + S + 1) % n`求得结点重新标号之前的旧编号,上述两步分析的公式整理如下: + + 1. 某一轮报数出列前:`编号 = (报数 - 1)% 出列前结点个数` + 2. 某一轮报数出列后:`旧编号 = (新编号 + 出列编号 + 1)% 出列前结点个数`,因为出列结点是因为报数`m`才出列的,所以有:`出列编号 = (m - 1)% 出列前结点个数` + 3. 由2可推出:`旧编号 = (新编号 + (m - 1)% 出列前结点个数 + 1)% 出列前结点个数` ,若用`n`表示**出列后**结点个数:`y = (x + (m - 1) % n + 1) % n = (x + m - 1) % n + 1` + +经过上面3步的复杂分析之后,我们得出这么一个通式:`旧编号 = (新编号 + m - 1 )% 出列前结点个数 + 1`,于是我们就可以自下而上(用链表模拟出列过程是自上而下),求出**最后一轮重新编号为`1`**的小朋友(只剩他一个了)在倒数第二轮重新编号时的旧编号,自下而上可倒推出这个小朋友在第一轮编号时(这时还没有任何一个小朋友出列过)的原始编号,即目标答案。 + +> 注意:式子`y = (x + m - 1) % n + 1`的计算结果不可能为`0`,因此我们可以按小朋友从`1`开始编号,将最后的计算结果应题目的要求(小朋友从0开始编号)减一个1即可。 + +```java +public int LastRemaining_Solution(int n, int m) { + if(n <= 0){ + //throw new IllegalArgumentException(); + return -1; + } + //最后一次重新编号:最后一个结点编号为1,出列前结点数为2 + return orginalNumber(2, 0, n, m); +} + +//根据出列后的重新编号(newNumber)推导出列前的旧编号(返回值) +//n:出列前有多少小朋友,N:总共有多少个小朋友 +public int orginalNumber(int n, int newNumber, int N, int m){ + int lastNumber = (newNumber + m - 1) % n + 1; + if(n == N){ + return lastNumber; + } + return orginalNumber(n + 1, lastNumber, N, m); +} +``` + +### 求1+2+3+…+n + +#### 题目描述 + +求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。 + +```java +public int Sum_Solution(int n) { + +} +``` + +#### 解析 + +##### 递归轻松解决 + +既然不允许遍历求和,不如将计算分解,如果知道了`f(n - 1)`,`f(n)`则可以通过`f(n - 1) + n`算出: + +```java +public int Sum_Solution(int n) { + if(n == 1){ + return 1; + } + return n + Sum_Solution(n - 1); +} +``` + +### 不用加减乘除做加法 + +#### 题目描述 + +写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。 + +#### 解析 + +不要忘了加减乘除是人类熟悉的运算方法,而计算机只知道位运算哦! + +我们可以将两数的二进制表示写出来,然后按位与得出进位信息、按位或得出非进位信息,如果进位信息不为0,则循环计算直到进位信息为0,此时异或信息就是两数之和: + +![image](https://ws2.sinaimg.cn/large/006zweohgy1fzeb2umgekj30hf0cvdgs.jpg) + +```java +public int Add(int num1,int num2) { + if(num1 == 0 || num2 == 0){ + return num1 == 0 ? num2 : num1; + } + int and = 0, xor = 0; + do{ + and = num1 & num2; + xor = num1 ^ num2; + num1 = and << 1; + num2 = xor; + }while(and != 0); + + return xor; +} +``` + +### 把字符串转换成整数 + +#### 题目描述 + +将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。 + +```java +public int StrToInt(String str) { + +} +``` + +#### 输入描述 + +输入一个字符串,包括数字字母符号,可以为空 + +#### 输出描述 + +如果是合法的数值表达则返回该数字,否则返回0 + +#### 示例 + +输入:`+2147483647`,输出:`2147483647` +输入:`1a33`,输出`0` + +#### 解析 + +1. 只有第一个位置上的字符可以是`+`或`-`或数字,其他位置上的字符必须是数字 +2. 如果第一个字符是`-`,返回结果必须是负数 +3. 如果字符串只有一个字符,且为`+`或`-`,这情况很容易被忽略 +4. 在对字符串解析转换时,如果发现溢出(包括正数向负数溢出,负数向正数溢出),必须有所处理(此时可以和面试官交涉),但不能视而不见 + +```java +public int StrToInt(String str) { + if(str == null || str.length() == 0){ + return 0; + } + boolean minus = false; + int index = 0; + if(str.charAt(0) == '-'){ + minus = true; + index = 1; + }else if(str.charAt(0) == '+'){ + index = 1; + } + //如果只有一个正负号 + if(index == str.length()){ + return 0; + } + + if(checkInteger(str, index, str.length() - 1)){ + return transform(str, index, str.length() - 1, minus); + } + + return 0; +} + +public boolean checkInteger(String str, int start, int end){ + if(str == null || str.length() == 0 || start < 0 || end > str.length() - 1 || start > end){ + return false; + } + for(int i = start ; i <= end ; i++){ + if(str.charAt(i) < '0' || str.charAt(i) > '9'){ + return false; + } + } + return true; +} + +public int transform(String str, int start, int end, boolean minus){ + if(str == null || str.length() == 0 || start < 0 || end > str.length() - 1 || start > end){ + throw new IllegalArgumentException(); + } + int res = 0; + for(int i = start ; i <= end ; i++){ + int num = str.charAt(i) - '0'; + res = minus ? (res * 10 - num) : (res * 10 + num); + if((minus && res > 0) || (!minus && res < 0)){ + throw new ArithmeticException("the str is overflow int"); + } + } + return res; +} +``` + +### 数组中重复的数字 + +#### 题目描述 + +在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。 + +```java +// Parameters: +// numbers: an array of integers +// length: the length of array numbers +// duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation; +// Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++ +// 这里要特别注意~返回任意重复的一个,赋值duplication[0] +// Return value: true if the input is valid, and there are some duplications in the array number +// otherwise false +public boolean duplicate(int numbers[],int length,int [] duplication) { + +} +``` + +#### 解析 + +认真审题发现输入数据是有特征的,即数组长度为`n`,数组中的元素都在`0~n-1`范围内,如果数组中没有重复的元素,那么排序后每个元素和其索引值相同,这就意味着数组中如果有重复的元素,那么数组排序后肯定有元素和它对应的索引是不等的。 + +顺着这个思路,我们可以将每个元素放到与它相等的索引上,如果某次放之前发现对应的索引上已有了和索引相同的元素,那么说明这个元素是重复的,由于每个元素最多会被调整两次,因此时间复杂`O(n)` + +```java +public boolean duplicate(int arr[],int length,int [] duplication) { + if(arr == null || arr.length == 0){ + return false; + } + int index = 0; + while(index < arr.length){ + if(arr[index] == arr[arr[index]]){ + if(index != arr[index]){ + duplication[0] = arr[index]; + return true; + }else{ + index++; + } + }else{ + int tmp = arr[index]; + arr[index] = arr[tmp]; + arr[tmp] = tmp; + } + } + + return false; +} +``` + +### 构建乘积数组 + +#### 题目描述 + +给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。 + +```java +public int[] multiply(int[] arr) { + +} +``` + + + +#### 分析 + +规律题: + +![image](https://ws2.sinaimg.cn/large/006zweohgy1fzee5lql6fj30ie0513ys.jpg) + +```java +public int[] multiply(int[] arr) { + if(arr == null || arr.length == 0){ + return arr; + } + int len = arr.length; + int[] arr1 = new int[len], arr2 = new int[len]; + arr1[0] = 1; + arr2[len - 1] = 1; + for(int i = 1 ; i < len ; i++){ + arr1[i] = arr1[i - 1] * arr[i - 1]; + arr2[len - 1 - i] = arr2[len - i] * arr[len - i]; + } + int[] res = new int[len]; + for(int i = 0 ; i < len ; i++){ + res[i] = arr1[i] * arr2[i]; + } + + return res; +} +``` + +### 正则表达式匹配 + +#### 题目描述 + +请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配 + +```java +public boolean match(char[] str, char[] pattern){ + +} +``` + + + +#### 解析 + +使用`p1`指向`str`中下一个要匹配的字符,使用`p2`指向`pattern`中剩下的模式串的首字符 + +1. 如果`p2 >= pattern.length`,表示模式串消耗完了,这时如果`p1`仍有字符要匹配那么返回`false`否则返回`true` +2. 如果`p1 >= str.length`,表示要匹配的字符都匹配完了,但模式串还没消耗完,这时剩下的模式串必须符合`a*b*c*`这样的范式以能够作为空串处理,否则返回`false` +3. `p1`和`p2`都未越界,按照`p2`后面是否是`*`来讨论 + 1. `p2`后面如果是`*`,又可按照`pattern[p2]`是否能够匹配`str[p1]`分析: + 1. `pattern[p2] == ‘.’ || pattern[p2] == str[p1]`,这时可以选择匹配一个`str[p1]`并继续向后匹配(不用跳过`p2`和其后面的`*`),也可以选择将`pattern[p2]`和其后面的`*`作为匹配空串处理,这时要跳过`p2`和 其后面的`*` + 2. `pattern[p2] != str[p1]`,只能作为匹配空串处理,跳过`p2` + 2. `p2`后面如果不是`*`: + 1. `pattern[p2] == str[p1] || pattern[p2] == ‘.’`,`p1,p2`同时后移一个继续匹配 + 2. `pattern[p2] == str[p1]`,直接返回`false` + +```java +public boolean match(char[] str, char[] pattern){ + if(str == null || pattern == null){ + return false; + } + if(str.length == 0 && pattern.length == 0){ + return true; + } + return matchCore(str, 0, pattern, 0); +} + +public boolean matchCore(char[] str, int p1, char[] pattern, int p2){ + //模式串用完了 + if(p2 >= pattern.length){ + return p1 >= str.length; + } + if(p1 >= str.length){ + if(p2 + 1 < pattern.length && pattern[p2 + 1] == '*'){ + return matchCore(str, p1, pattern, p2 + 2); + }else{ + return false; + } + } + + //如果p2的后面是“*” + if(p2 + 1 < pattern.length && pattern[p2 + 1] == '*'){ + if(pattern[p2] == '.' || pattern[p2] == str[p1]){ + //匹配一个字符,接着还可以向后匹配;或者将当前字符和后面的星合起来做空串 + return matchCore(str, p1 + 1, pattern, p2) || matchCore(str, p1, pattern, p2 + 2); + }else{ + return matchCore(str, p1, pattern, p2 + 2); + } + } + //如果p2的后面不是* + else{ + if(pattern[p2] == '.' || pattern[p2] == str[p1]){ + return matchCore(str, p1 + 1, pattern, p2 + 1); + }else{ + return false; + } + } +} +``` + +### 表示数值的字符串 + +#### 题目描述 + +请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。 + +```java +public boolean isNumeric(char[] str) { + +} +``` + +#### 解析 + +由题式可得出如下约束: + +1. 正负号只能出现在第一个位置或者`e/E`后一个位置 +2. `e/E`后面有且必须有整数 +3. 字符串中只能包含数字、小数点、正负号、`e/E`,其它的都是非法字符 +4. `e/E`的前面最多只能出现一次小数点,而`e/E`的后面不能出现小数点 + +```java +public boolean isNumeric(char[] str) { + if(str == null || str.length == 0){ + return false; + } + + boolean signed = false; //标识是否以正负号开头 + boolean decimal = false; //标识是否有小数点 + boolean existE = false; //是否含有e/E + int start = -1; //一段连续数字的开头 + int index = 0; //从0开始遍历字符 + + if(existSignAtIndex(str, 0)){ + signed = true; + index++; + } + + while(index < str.length){ + //以下按照index上可能出现的字符进行分支判断 + if(str[index] >= '0' && str[index] <= '9'){ + start = (start == -1) ? index : start; + index++; + + }else if(str[index] == '+' || str[index] == '-'){ + //首字符的+-我们已经判断过了,因此+-只可能出现在e/E的后面 + if(!existEAtIndex(str, index - 1)){ + return false; + } + index++; + + }else if(str[index] == '.'){ + //小数点只可能出现在e/E前面,且只可能出现一次 + //如果出现过小数点了,或者小数点前一段连续数字的前面是e/E + if(decimal || existEAtIndex(str, start - 1) + || existEAtIndex(str, start - 2) ){ + return false; + } + decimal = true;//出现了小数点 + index++; + //下一段连续数字的开始 + start = index; + + }else if(existEAtIndex(str, index)){ + if(existE){ + //如果已出现过e/E + return false; + } + existE = true; + index++; + //由于e/E后面可能是正负号也可能是数字,所以下一段连续数字的开始不确定 + start = !existSignAtIndex(str, index) ? index : index + 1; + + }else{ + return false; + } + } + + //如果最后一段连续数字的开始不存在 -> e/E后面没有数字 + if(start >= str.length){ + return false; + } + + return true; +} + +//在index上的字符是否是e或者E +public boolean existEAtIndex(char[] str, int index){ + if(str == null || str.length == 0 || index < 0 || index > str.length - 1){ + return false; + } + return str[index] == 'e' || str[index] == 'E'; +} + +//在index上的字符是否是正负号 +public boolean existSignAtIndex(char[] str, int index){ + if(str == null || str.length == 0 || index < 0 || index > str.length - 1){ + return false; + } + return str[index] == '+' || str[index] == '-'; +} +``` + + + +### 字符流中第一个只出现一次的字符 + +#### 题目描述 + +请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。 + +#### 输出描述 + +如果当前字符流没有存在出现一次的字符,返回#字符。 + +#### 解析 + +首先要选取一个容器来保存字符,并且要记录字符进入容器的顺序。如果不考虑中文字符,那么可以使用一张大小为`256`(对应`ASCII`码值范围)的表来保存字符,用字符的`ASCII`**码值**作为索引,用字符进入容器的**次序**作为索引对应的记录,表内部维护了一个计数器`position`,每当有字符进入时以该计数器的值作为该字符的次序(初始时,每个字符对应的次序为-1),如果设置该字符的次序时发现之前已设置过(次序不为-1,而是大于等于0),那么将该字符的次序置为-2,表示以后从容器取第一个只出现一次的字符时不考虑该字符。 + +当从容器取第一个只出现一次的字符时,考虑次序大于等于0的字符,在这个前提下找出次序最小的字符并返回。 + +```java +//不算中文,保存所有ascii码对应的字符只需256字节,记录ascii码为index的字符首次出现的位置 +int[] arr = new int[256]; +int position = 0; +{ + for(int i = 0 ; i < arr.length ; i++){ + //初始时所有字符的首次出现的位置为-1 + arr[i] = -1; + } +} +//Insert one char from stringstream +public void Insert(char ch){ + int ascii = (int)ch; + if(arr[ascii] == -1){ + arr[ascii] = position++; + }else if(arr[ascii] >= 0){ + arr[ascii] = -2; + } +} +//return the first appearence once char in current stringstream +public char FirstAppearingOnce(){ + int minPosi = Integer.MAX_VALUE; + char res = '#'; + for(int i = 0 ; i < arr.length ; i++){ + if(arr[i] >= 0 && arr[i] < minPosi){ + minPosi = arr[i]; + res = (char)i; + } + } + + return res; +} +``` + +### 删除链表中重复的结点 + +#### 题目描述 + +在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5 + +```java +public ListNode deleteDuplication(ListNode pHead){ + +} +``` + +#### 解析 + +此题处理起来棘手的有两个地方: + +1. 如果某个结点的后继结点与其重复,那么删除该结点的一串连续重复的结点之后如何删除该结点本身,这就要求我们需要保留当前遍历结点的前驱指针。 + + 但是如果从头结点开始就出现一连串的重复呢?我们又如何删除删除头结点,因此我们需要新建一个辅助结点作为头结点的前驱结点。 + +2. 在遍历过程中如何区分当前结点是不重复的结点,还是在删除了它的若干后继结点之后最后也要删除它本身的重复结点?这就需要我们使用一个布尔变量记录是否开启了删除模式(`deleteMode`) + +经过上述两步分析,我们终于可以安心遍历结点了: + +```java +public ListNode deleteDuplication(ListNode pHead){ + if(pHead == null){ + return null; + } + ListNode node = new ListNode(Integer.MIN_VALUE); + node.next = pHead; + ListNode pre = node, p = pHead; + boolean deletedMode = false; + while(p != null){ + if(p.next != null && p.next.val == p.val){ + p.next = p.next.next; + deletedMode = true; + }else if(deletedMode){ + pre.next = p.next; + p = pre.next; + deletedMode = false; + }else{ + pre = p; + p = p.next; + } + } + + return node.next; +} +``` + +### 二叉树的下一个结点 + +#### 题目描述 + +给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 + +#### 解析 + +由于中序遍历来到某个结点后,首先会接着遍历它的右子树,如果它没有右子树则会回到祖先结点中将它当做左子树上的结点的那一个,因此有如下分析: + +1. 如果当前结点有右子树,那么其后继结点就是其右子树上的最左结点 +2. 如果当前结点没有右子树,那么其后继结点就是其祖先结点中,将它当做左子树上的结点的那一个。 + +```java +public TreeLinkNode GetNext(TreeLinkNode pNode){ + if(pNode == null){ + return null; + } + //如果有右子树,后继结点是右子树上最左的结点 + if(pNode.right != null){ + TreeLinkNode p = pNode.right; + while(p.left != null){ + p = p.left; + } + return p; + }else{ + //如果没有右子树,向上查找第一个当前结点是父结点的左孩子的结点 + TreeLinkNode p = pNode.next; + while(p != null && pNode != p.left){ + pNode = p; + p = p.next; + } + + if(p != null && pNode == p.left){ + return p; + } + return null; + } +} +``` + +​ + +### 对称的二叉树 + +#### 题目描述 + +请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。 + +```java +boolean isSymmetrical(TreeNode pRoot){ + +} +``` + +#### 解析 + +判断一棵树是否是镜像二叉树,只需将经典的先序遍历序列和变种的**先根再右再左**的先序遍历序列比较,如果相同则为镜像二叉树。 + +```java +boolean isSymmetrical(TreeNode pRoot){ + if(pRoot == null){ + return true; + } + StringBuffer str1 = new StringBuffer(""); + StringBuffer str2 = new StringBuffer(""); + preOrder(pRoot, str1); + preOrder2(pRoot, str2); + return str1.toString().equals(str2.toString()); +} + +public void preOrder(TreeNode root, StringBuffer str){ + if(root == null){ + str.append("#"); + return; + } + str.append(String.valueOf(root.val)); + preOrder(root.left, str); + preOrder(root.right, str); +} + +public void preOrder2(TreeNode root, StringBuffer str){ + if(root == null){ + str.append("#"); + return; + } + str.append(String.valueOf(root.val)); + preOrder2(root.right, str); + preOrder2(root.left, str); +} +``` + +### 按之字形打印二叉树 + +#### 题目描述 + +请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。 + +#### 解析 + +注意下述代码的第`14`行,笔者曾写为`stack2 = stack1 == empty ? stack1 : stack2`,你能发现错误在哪儿吗? + +```java +public ArrayList> Print(TreeNode pRoot) { + ArrayList> res = new ArrayList(); + if(pRoot == null){ + return res; + } + + Stack stack1 = new Stack(); + Stack stack2 = new Stack(); + stack1.push(pRoot); + boolean flag = true;//先加左孩子,再加右孩子 + while(!stack1.empty() || !stack2.empty()){ + Stack empty = stack1.empty() ? stack1 : stack2; + stack1 = stack1 == empty ? stack2 : stack1; + stack2 = empty; + ArrayList row = new ArrayList(); + while(!stack1.empty()){ + TreeNode p = stack1.pop(); + row.add(p.val); + if(flag){ + if(p.left != null){ + stack2.push(p.left); + } + if(p.right != null){ + stack2.push(p.right); + } + }else{ + if(p.right != null){ + stack2.push(p.right); + } + if(p.left != null){ + stack2.push(p.left); + } + } + } + res.add(row); + flag = !flag; + } + + return res; +} +``` + +### 序列化二叉树 + +#### 题目描述 + +请实现两个函数,分别用来序列化和反序列化二叉树 + +#### 解析 + +怎么序列化的,就怎么反序列化。这里`deserialize`反序列化时对于序列化到`String[] arr`的哪个结点值来了的变量`index`有两个坑(都是笔者亲自踩的): + +1. 将`index`声明为成员的`int`,`Java`中函数调用时不会改变基本类型参数的值的,因此不要企图使用`int`表示当前序列化哪个结点的值来了 +2. 之后笔者想用`Integer`代替,但是`Integer`和`String`一样,都是不可变对象,所有的值更改操作在底层都是拆箱和装箱生成新的`Integer`,因此也不要使用`Integer`做序列化到哪一个结点数值来了的计数器 +3. 最好使用数组或者自定义的类(在类中声明一个`int`变量) + +```java +String Serialize(TreeNode root) { + if(root == null){ + return "#_"; + } + //处理头结点、左子树、右子树 + String res = root.val + "_"; + res += Serialize(root.left); + res += Serialize(root.right); + return res; +} + +TreeNode Deserialize(String str) { + if(str == null || str.length() == 0){ + return null; + } + Integer index = 0; + return deserialize(str.split("_"), new int[]{0}); +} + +//怎么序列化的,就怎么反序列化 +TreeNode deserialize(String[] arr, int[] index){ + if("#".equals(arr[index[0]])){ + index[0]++; + return null; + } + //头结点、左子树、右子树 + TreeNode root = new TreeNode(Integer.parseInt(arr[index[0]])); + index[0]++; + root.left = deserialize(arr, index); + root.right = deserialize(arr, index); + return root; +} +``` + +### 二叉搜索树的第k个结点 + +#### 题目描述 + +给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。 + +```java +TreeNode KthNode(TreeNode pRoot, int k){ + +} +``` + +#### 解析 + +二叉搜索树的特点是,它的中序序列是有序的,因此我们可以借助中序遍历在递归体中第二次来到当前结点时更新一下计数器,直到遇到第k个结点保存并返回即可。 + +值得注意的地方是: + +1. 由于计数器在递归中传来传去,因此你需要保证每个递归引用的是同一个计数器,这里使用的是一个`int[]`的第一个元素来保存 +2. 我们写中序遍历是不需要返回值的,可以在找到第k小的结点时将其保存在传入的数组中以返回给调用方 + +```java +TreeNode KthNode(TreeNode pRoot, int k){ + if(pRoot == null){ + return null; + } + TreeNode[] res = new TreeNode[1]; + inOrder(pRoot, new int[]{ k }, res); + return res[0]; +} + +public void inOrder(TreeNode root, int[] count, TreeNode[] res){ + if(root == null){ + return; + } + inOrder(root.left, count, res); + count[0]--; + if(count[0] == 0){ + res[0] = root; + return; + } + inOrder(root.right, count, res); +} +``` + +> 如果可以利用我们熟知的算法,比如本题中的中序遍历。管它三七二十一先将熟知方法写出来,然后再按具体的业务需求对其进行改造(包括返回值、参数列表,但一般不会更改遍历算法的返回值) + +### 数据流的中位数 + +#### 题目描述 + +如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。 + +```java +public void Insert(Integer num) { + +} + +public Double GetMedian() { + +} +``` + +#### 解析 + +由于中位数只与排序后位于数组中间的一个数或两个数相关,而与数组两边的其它数无关,因此我们可以用一个大根堆保存数组左半边的数的最大值,用一个小根堆保存数组右半边的最小值,插入元素`O(logn)`,取中位数`O(1)`。 + +```java +public class Solution { + + //小根堆、大根堆 + PriorityQueue minHeap = new PriorityQueue(new MinRootHeadComparator()); + PriorityQueue maxHeap = new PriorityQueue(new MaxRootHeadComparator()); + int count = 0; + + class MaxRootHeadComparator implements Comparator{ + //返回值大于0则认为逻辑上i2大于i1(无关对象包装的数值) + public int compare(Integer i1, Integer i2){ + return i2.intValue() - i1.intValue(); + } + } + + class MinRootHeadComparator implements Comparator{ + public int compare(Integer i1, Integer i2){ + return i1.intValue() - i2.intValue(); + } + } + + public void Insert(Integer num) { + count++;//当前这个数是第几个进来的 + //编号是奇数就放入小根堆(右半边),否则放入大根堆 + if(count % 2 != 0){ + //如果要放入右半边的数比左半边的最大值要小则需调整左半边的最大值放入右半边并将当前这个数放入左半边,这样才能保证右半边的数都比左半边的大 + if(maxHeap.size() > 0 && num < maxHeap.peek()){ + maxHeap.add(num); + num = maxHeap.poll(); + } + minHeap.add(num); + }else{ + if(minHeap.size() > 0 && num > minHeap.peek()){ + minHeap.add(num); + num = minHeap.poll(); + } + maxHeap.add(num); + } + } + + public Double GetMedian() { + if(count == 0){ + return 0.0; + } + if(count % 2 != 0){ + return minHeap.peek().doubleValue(); + }else{ + return (minHeap.peek().doubleValue() + maxHeap.peek().doubleValue()) / 2; + } + } +} +``` + +### 滑动窗口的最大值 + +#### 题目描述 + +给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。 + +```java +public ArrayList maxInWindows(int [] num, int size){ + +} +``` + +#### 解析 + +使用一个单调非增队列,队头保存当前窗口的最大值,后面保存在窗口移动过程中导致队头失效(出窗口)后的从而晋升为窗口最大值的候选值。 + +```java +public ArrayList maxInWindows(int [] num, int size){ + ArrayList res = new ArrayList(); + if(num == null || num.length == 0 || size <= 0 || size > num.length){ + return res; + } + + //用队头元素保存窗口最大值,队列中元素只能是单调递减的,窗口移动可能导致队头元素失效 + LinkedList queue = new LinkedList(); + int start = 0, end = size - 1; + for(int i = start ; i <= end ; i++){ + addLast(queue, num[i]); + } + res.add(queue.getFirst()); + //移动窗口 + while(end < num.length - 1){ + addLast(queue, num[++end]); + if(queue.getFirst() == num[start]){ + queue.pollFirst(); + } + start++; + res.add(queue.getFirst()); + } + + return res; +} + +public void addLast(LinkedList queue, int num){ + if(queue == null){ + return; + } + //加元素之前要确保该元素小于等于队尾元素 + while(queue.size() != 0 && num > queue.getLast()){ + queue.pollLast(); + } + queue.addLast(num); +} +``` + +### 矩形中的路径 + +#### 题目描述 + +请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。 例如 a b c e s f c s a d e e 这样的3 X 4 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。 + +#### 解析 + +定义一个黑盒`hasPathCorechar(matrix, rows, cols, int i, int j, str, index)`,表示从`rows`行`cols`列的矩阵`matrix`中的`(i,j)`位置开始走是否能走出一条与`str`的子串`index ~ str.length-1`相同的路径。那么对于当前位置`(i,j)`,需要关心的只有一下三点: + +1. `(i,j)`是否越界了 +2. `(i,j)`上的字符是否和`str[index]`匹配 +3. `(i,j)`是否已在之前走过的路径上 + +如果通过了上面三点检查,那么认为`(i,j)`这个位置是可以走的,剩下的就是`(i,j)`上下左右四个方向能否走出`str`的`index+1 ~ str.length-1`,这个交给黑盒就好了。 + +还有一点要注意,如果确定了可以走当前位置`(i,j)`,那么需要将该位置的`visited`标记为`true`,表示该位置在已走过的路径上,而退出`(i,j)`的时候(对应下面第`32`行)又要将他的`visited`重置为`false`。 + +```java +public boolean hasPath(char[] matrix, int rows, int cols, char[] str){ + if(matrix == null || matrix.length != rows * cols || str == null){ + return false; + } + boolean[] visited = new boolean[matrix.length]; + for(int i = 0 ; i < rows ; i++){ + for(int j = 0 ; j < cols ; j++){ + //以矩阵中的每个点作为起点尝试走出str对应的路径 + if(hasPathCore(matrix, rows, cols, i, j, str, 0, visited)){ + return true; + } + } + } + return false; +} + +//当前在矩阵的(i,j)位置上 +//index -> 匹配到了str中的第几个字符 +private boolean hasPathCore(char[] matrix, int rows, int cols, int i, int j, + char[] str, int index, boolean[] visited){ + if(index == str.length){ + return true; + } + //越界或字符不匹配或该位置已在路径上 + if(!match(matrix, rows, cols, i, j, str[index]) || visited[i * cols + j] == true){ + return false; + } + visited[i * cols + j] = true; + boolean res = hasPathCore(matrix, rows, cols, i + 1, j, str, index + 1, visited) || + hasPathCore(matrix, rows, cols, i - 1, j, str, index + 1, visited) || + hasPathCore(matrix, rows, cols, i, j + 1, str, index + 1, visited) || + hasPathCore(matrix, rows, cols, i, j - 1, str, index + 1, visited); + visited[i * cols + j] = false; + return res; +} + +//矩阵matrix中的(i,j)位置上是否是c字符 +private boolean match(char[] matrix, int rows, int cols, int i, int j, char c){ + if(i < 0 || i > rows - 1 || j < 0 || j > cols - 1){ + return false; + } + return matrix[i * cols + j] == c; +} +``` + +### 机器人的运动范围 + +#### 题目描述 + +地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子? + +#### 解析 + +定义一个黑盒`walk(int threshold, int rows, int cols, int i, int j, boolean[] visited)`,它表示统计从`rows`行`cols`列的矩阵中的`(i,j)`开始所能到达的格子并返回,对于当前位置`(i,j)`有如下判断: + +1. `(i,j)`是否越界矩阵了 +2. `(i,j)`是否已被统计过了 +3. `(i,j)`的行坐标和列坐标的数位之和是否大于`k` + +如果通过了上面三重检查,则认为`(i,j)`是可以到达的(`res=1`),并标记`(i,j)`的`visited`为`true`表示已被统计过了,然后对`(i,j)`的上下左右的格子调用黑盒进行统计。 + +这里要注意的是,与上一题不同,`visited`不会在递归计算完子状态后被重置回`false`,因为每个格子只能被统计一次。`visited`的含义不一样 + +```java +public int movingCount(int threshold, int rows, int cols){ + if(threshold < 0 || rows < 0 || cols < 0){ + return 0; + } + boolean[] visited = new boolean[rows * cols]; + return walk(threshold, rows, cols, 0, 0, visited); +} + +private int walk(int threshold, int rows, int cols, int i, int j, boolean[] visited){ + if(!isLegalPosition(rows, cols, i, j) || visited[i * cols + j] == true + || bitSum(i) + bitSum(j) > threshold){ + return 0; + } + int res = 1; + visited[i * cols + j] = true; + res += walk(threshold, rows, cols, i + 1, j, visited) + + walk(threshold, rows, cols, i - 1, j, visited) + + walk(threshold, rows, cols, i, j + 1, visited) + + walk(threshold, rows, cols, i, j - 1, visited); + return res; +} + +private boolean isLegalPosition(int rows, int cols, int i, int j){ + if(i < 0 || j < 0 || i > rows - 1 || j > cols - 1){ + return false; + } + return true; +} + +public int bitSum(int num){ + int res = num % 10; + while((num /= 10) != 0){ + res += num % 10; + } + return res; +} +``` + From 46940b25e8a8014e24e372b5b821f90bd6be7864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sun, 11 Aug 2019 23:46:54 +0800 Subject: [PATCH 014/323] =?UTF-8?q?Create=20MySQL=E4=BC=98=E5=8C=96.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "MySQL\344\274\230\345\214\226.md" | 1783 ++++++++++++++++++++++++++++ 1 file changed, 1783 insertions(+) create mode 100644 "MySQL\344\274\230\345\214\226.md" diff --git "a/MySQL\344\274\230\345\214\226.md" "b/MySQL\344\274\230\345\214\226.md" new file mode 100644 index 00000000..9c6ff442 --- /dev/null +++ "b/MySQL\344\274\230\345\214\226.md" @@ -0,0 +1,1783 @@ +--- +title: MySQL优化看这一篇就够了 +date: 2018-12-25 11:04:03 +updated_at: +comments: true +photos: "http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-29/51481063.jpg" +categories: 数据库 +tags: MySQL +--- + +# 本文概要 + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-30/53535419.jpg) + +# 概述 + +## 为什么要优化 + +- 系统的吞吐量瓶颈往往出现在数据库的访问速度上 +- 随着应用程序的运行,数据库的中的数据会越来越多,处理时间会相应变慢 +- 数据是存放在磁盘上的,读写速度无法和内存相比 + +## 如何优化 + +- 设计数据库时:数据库表、字段的设计,存储引擎 +- 利用好MySQL自身提供的功能,如索引等 +- 横向扩展:MySQL集群、负载均衡、读写分离 +- SQL语句的优化(收效甚微) + +# 字段设计 + +> 字段类型的选择,设计规范,范式,常见设计案例 + +## 原则:尽量使用整型表示字符串 + +### 存储IP + +`INET_ATON(str)`,address to number + +`INET_NTOA(number)`,number to address + +### MySQL内部的枚举类型(单选)和集合(多选)类型 + +但是因为维护成本较高因此不常使用,使用**关联表**的方式来替代`enum` + +## 原则:定长和非定长数据类型的选择 + +> decimal不会损失精度,存储空间会随数据的增大而增大。double占用固定空间,较大数的存储会损失精度。非定长的还有varchar、text + +### 金额 + +> 对数据的精度要求较高,小数的运算和存储存在精度问题(不能将所有小数转换成二进制) + +### 定点数decimal + +`price decimal(8,2)`有2位小数的定点数,定点数支持很大的数(甚至是超过`int,bigint`存储范围的数) + +### 小单位大数额避免出现小数 + +元->分 + +### 字符串存储 + +定长`char`,非定长`varchar、text`(上限65535,其中`varchar`还会消耗1-3字节记录长度,而`text`使用额外空间记录长度) + +## 原则:尽可能选择小的数据类型和指定短的长度 + +## 原则:尽可能使用 not null + +非`null`字段的处理要比`null`字段的处理高效些!且不需要判断是否为`null`。 + +`null`在MySQL中,不好处理,存储需要额外空间,运算也需要特殊的运算符。如`select null = null`和`select null <> null`(`<>`为不等号)有着同样的结果,只能通过`is null`和`is not null`来判断字段是否为`null`。 + +如何存储?MySQL中每条记录都需要额外的存储空间,表示每个字段是否为`null`。因此通常使用特殊的数据进行占位,比如`int not null default 0`、`string not null default ‘’` + +## 原则:字段注释要完整,见名知意 + +## 原则:单表字段不宜过多 + +二三十个就极限了 + +## 原则:可以预留字段 + + + +> 在使用以上原则之前首先要满足业务需求 + +# 关联表的设计 + +> 外键`foreign key`只能实现一对一或一对多的映射 + +## 一对多 + +使用外键 + +## 多对多 + +单独新建一张表将多对多拆分成两个一对多 + +## 一对一 + +如商品的基本信息(`item`)和商品的详细信息(`item_intro`),通常使用相同的主键或者增加一个外键字段(`item_id`) + +# 范式 Normal Format + +> 数据表的设计规范,一套越来越严格的规范体系(如果需要满足N范式,首先要满足N-1范式)。N + +## 第一范式1NF:字段原子性 + +字段原子性,字段不可再分割。 + +> 关系型数据库,默认满足第一范式 + +注意比较容易出错的一点,在一对多的设计中使用逗号分隔多个外键,这种方法虽然存储方便,但不利于维护和索引(比如查找带标签`java`的文章) + +## 第二范式:消除对主键的部分依赖 + +> 即在表中加上一个与业务逻辑无关的字段作为主键 + +主键:可以唯一标识记录的字段或者字段集合。 + +| course_name | course_class | weekday(周几) | course_teacher | +| ----------- | ------------ | --------------- | -------------- | +| MySQL | 教育大楼1525 | 周一 | 张三 | +| Java | 教育大楼1521 | 周三 | 李四 | +| MySQL | 教育大楼1521 | 周五 | 张三 | + +依赖:A字段可以确定B字段,则B字段依赖A字段。比如知道了下一节课是数学课,就能确定任课老师是谁。于是**周几**和**下一节课**和就能构成复合主键,能够确定去哪个教室上课,任课老师是谁等。但我们常常增加一个`id`作为主键,而消除对主键的部分依赖。 + +对主键的部分依赖:某个字段依赖复合主键中的一部分。 + +解决方案:新增一个独立字段作为主键。 + +## 第三范式:消除对主键的传递依赖 + +传递依赖:B字段依赖于A,C字段又依赖于B。比如上例中,任课老师是谁取决于是什么课,是什么课又取决于主键`id`。因此需要将此表拆分为两张表日程表和课程表(独立数据独立建表): + +| id | weekday | course_class | course_id | +| ---- | ------- | ------------ | --------- | +| 1001 | 周一 | 教育大楼1521 | 3546 | + +| course_id | course_name | course_teacher | +| --------- | ----------- | -------------- | +| 3546 | Java | 张三 | + +这样就减少了数据的冗余(即使周一至周日每天都有Java课,也只是`course_id:3546`出现了7次) + +# 存储引擎选择 + +> 早期问题:如何选择MyISAM和Innodb? +> +> 现在不存在这个问题了,Innodb不断完善,从各个方面赶超MyISAM,也是MySQL默认使用的。 + +存储引擎Storage engine:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。 + +## 功能差异 + +`show engines` + +| Engine | Support | Comment | +| ------ | ------- | ------------------------------------------------------------ | +| InnoDB | DEFAULT | **Supports transactions, row-level locking, and foreign keys** | +| MyISAM | YES | **MyISAM storage engine** | + +## 存储差异 + +| | MyISAM | Innodb | +| ------------------------------------------------------------ | ------------------------------------------------- | ---------------------------------------- | +| 文件格式 | 数据和索引是分别存储的,数据`.MYD`,索引`.MYI` | 数据和索引是集中存储的,`.ibd` | +| 文件能否移动 | 能,一张表就对应`.frm`、`MYD`、`MYI`3个文件 | 否,因为关联的还有`data`下的其它文件 | +| 记录存储顺序 | 按记录插入顺序保存 | 按主键大小有序插入 | +| 空间碎片(删除记录并`flush table 表名`之后,表文件大小不变) | 产生。定时整理:使用命令`optimize table 表名`实现 | 不产生 | +| 事务 | 不支持 | 支持 | +| 外键 | 不支持 | 支持 | +| 锁支持(锁是避免资源争用的一个机制,MySQL锁对用户几乎是透明的) | 表级锁定 | 行级锁定、表级锁定,锁定力度小并发能力高 | + +> 锁扩展 +> +> 表级锁(`table-level lock`):`lock tables ,... read/write`,`unlock tables ,...`。其中`read`是共享锁,一旦锁定任何客户端都不可读;`write`是独占/写锁,只有加锁的客户端可读可写,其他客户端既不可读也不可写。锁定的是一张表或几张表。 +> +> 行级锁(`row-level lock`):锁定的是一行或几行记录。共享锁:`select * from where <条件> LOCK IN SHARE MODE;`,对查询的记录增加共享锁;`select * from where <条件> FOR UPDATE;`,对查询的记录增加排他锁。这里**值得注意**的是:`innodb`的行锁,其实是一个子范围锁,依据条件锁定部分范围,而不是就映射到具体的行上,因此还有一个学名:间隙锁。比如`select * from stu where id < 20 LOCK IN SHARE MODE`会锁定`id`在`20`左右以下的范围,你可能无法插入`id`为`18`或`22`的一条新纪录。 + +## 选择依据 + +如果没有特别的需求,使用默认的`Innodb`即可。 + +MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。 + +Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键保证数据完整性。比如OA自动化办公系统。 + +# 索引 + +> 关键字与数据的映射关系称为索引(==包含关键字和对应的记录在磁盘中的地址==)。关键字是从数据当中提取的用于标识、检索数据的特定内容。 + +## 索引检索为什么快? + +- 关键字相对于数据本身,==数据量小== +- 关键字是==有序==的,二分查找可快速确定位置 + +图书馆为每本书都加了索引号(类别-楼层-书架)、字典为词语解释按字母顺序编写目录等都用到了索引。 + +## MySQL中索引类型 + +> **普通索引**(`key`),**唯一索引**(`unique key`),**主键索引**(`primary key`),**全文索引**(`fulltext key`) + +三种索引的索引方式是一样的,只不过对索引的关键字有不同的限制: + +- 普通索引:对关键字没有限制 +- 唯一索引:要求记录提供的关键字不能重复 +- 主键索引:要求关键字唯一且不为null + +## 索引管理语法 + +### 查看索引 + +`show create table 表名`: + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/51457246.jpg) + +`desc 表名` + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/97005334.jpg) + +### 创建索引 + +#### 创建表之后建立索引 + +```sql +create TABLE user_index( + id int auto_increment primary key, + first_name varchar(16), + last_name VARCHAR(16), + id_card VARCHAR(18), + information text +); + +-- 更改表结构 +alter table user_index +-- 创建一个first_name和last_name的复合索引,并命名为name +add key name (first_name,last_name), +-- 创建一个id_card的唯一索引,默认以字段名作为索引名 +add UNIQUE KEY (id_card), +-- 鸡肋,全文索引不支持中文 +add FULLTEXT KEY (information); +``` + +`show create table user_index`: + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/87637544.jpg) + +#### 创建表时指定索引 + +```sql +CREATE TABLE user_index2 ( + id INT auto_increment PRIMARY KEY, + first_name VARCHAR (16), + last_name VARCHAR (16), + id_card VARCHAR (18), + information text, + KEY name (first_name, last_name), + FULLTEXT KEY (information), + UNIQUE KEY (id_card) +); +``` + +### 删除索引 + +根据索引名删除普通索引、唯一索引、全文索引:`alter table 表名 drop KEY 索引名` + +```sql +alter table user_index drop KEY name; +alter table user_index drop KEY id_card; +alter table user_index drop KEY information; +``` + +删除主键索引:`alter table 表名 drop primary key`(因为主键只有一个)。这里值得注意的是,如果主键自增长,那么不能直接执行此操作(自增长依赖于主键索引): + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/22392054.jpg) + +需要取消自增长再行删除: + +```sql +alter table user_index +-- 重新定义字段 +MODIFY id int, +drop PRIMARY KEY +``` + +但通常不会删除主键,因为设计主键一定与业务逻辑无关。 + +## 执行计划explain + +```sql +CREATE TABLE innodb1 ( + id INT auto_increment PRIMARY KEY, + first_name VARCHAR (16), + last_name VARCHAR (16), + id_card VARCHAR (18), + information text, + KEY name (first_name, last_name), + FULLTEXT KEY (information), + UNIQUE KEY (id_card) +); +insert into innodb1 (first_name,last_name,id_card,information) values ('张','三','1001','华山派'); +``` + +我们可以通过`explain selelct`来分析SQL语句执行前的执行计划: + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/66167137.jpg) + +由上图可看出此SQL语句是按照主键索引来检索的。 + +执行计划是:当执行SQL语句时,首先会分析、优化,形成执行计划,在按照执行计划执行。 + +## 索引使用场景(重点) + +### where + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/4492079.jpg) + +上图中,根据`id`查询记录,因为`id`字段仅建立了主键索引,因此此SQL执行可选的索引只有主键索引,如果有多个,最终会选一个较优的作为检索的依据。 + +```sql +-- 增加一个没有建立索引的字段 +alter table innodb1 add sex char(1); +-- 按sex检索时可选的索引为null +EXPLAIN SELECT * from innodb1 where sex='男'; +``` + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/33916825.jpg) + +> 可以尝试在一个字段未建立索引时,根据该字段查询的效率,然后对该字段建立索引(`alter table 表名 add index(字段名)`),同样的SQL执行的效率,你会发现查询效率会有明显的提升(数据量越大越明显)。 + +### order by + +当我们使用`order by`将查询结果按照某个字段排序时,如果该字段没有建立索引,那么执行计划会将查询出的所有数据使用外部排序(将数据从硬盘分批读取到内存使用内部排序,最后合并排序结果),这个操作是很影响性能的,因为需要将查询涉及到的所有数据从磁盘中读到内存(如果单条数据过大或者数据量过多都会降低效率),更无论读到内存之后的排序了。 + +但是如果我们对该字段建立索引`alter table 表名 add index(字段名)`,那么由于索引本身是有序的,因此直接按照索引的顺序和映射关系逐条取出数据即可。而且如果分页的,那么只用**取出索引表某个范围内的索引对应的数据**,而不用像上述那**取出所有数据**进行排序再返回某个范围内的数据。(从磁盘取数据是最影响性能的) + +### join + +> 对`join`语句匹配关系(`on`)涉及的字段建立索引能够提高效率 + +### 索引覆盖 + +如果要查询的字段都建立过索引,那么引擎会直接在索引表中查询而不会访问原始数据(否则只要有一个字段没有建立索引就会做全表扫描),这叫索引覆盖。因此我们需要尽可能的在`select`后==只写必要的查询字段==,以增加索引覆盖的几率。 + +这里值得注意的是不要想着为每个字段建立索引,因为优先使用索引的优势就在于其体积小。 + +## 语法细节(要点) + +> 在满足索引使用的场景下(`where/order by/join on`或索引覆盖),索引也不一定被使用 + +### 字段要独立出现 + +比如下面两条SQL语句在语义上相同,但是第一条会使用主键索引而第二条不会。 + +```sql +select * from user where id = 20-1; +select * from user where id+1 = 20; +``` + +### `like`查询,不能以通配符开头 + +比如搜索标题包含`mysql`的文章: + +```sql +select * from article where title like '%mysql%'; +``` + +这种SQL的执行计划用不了索引(`like`语句匹配表达式以通配符开头),因此只能做全表扫描,效率极低,在实际工程中几乎不被采用。而一般会使用第三方提供的支持中文的全文索引来做。 + +但是 **关键字查询** 热搜提醒功能还是可以做的,比如键入`mysql`之后提醒`mysql 教程`、`mysql 下载`、`mysql 安装步骤`等。用到的语句是: + +```sql +select * from article where title like 'mysql%'; +``` + +这种`like`是可以利用索引的(当然前提是`title`字段建立过索引)。 + +### 复合索引只对第一个字段有效 + +建立复合索引: + +```sql +alter table person add index(first_name,last_name); +``` + +其原理就是将索引先按照从`first_name`中提取的关键字排序,如果无法确定先后再按照从`last_name`提取的关键字排序,也就是说该索引表只是按照记录的`first_name`字段值有序。 + +因此`select * from person where first_name = ?`是可以利用索引的,而`select * from person where last_name = ?`无法利用索引。 + +> 那么该复合索引的应用场景是什么?==组合查询== + +比如对于`select * person from first_name = ? and last_name = ?`,复合索引就比对`first_name`和`last_name`单独建立索引要高效些。很好理解,复合索引首先二分查找与`first_name = ?`匹配的记录,再在这些记录中二分查找与`last_name`匹配的记录,只涉及到一张索引表。而分别单独建立索引则是在`first_name`索引表中二分找出与`first_name = ?`匹配的记录,再在`last_name`索引表中二分找出与`last_name = ?`的记录,两者取交集。 + +### or,两边条件都有索引可用 + +> 一但有一边无索引可用就会导致整个SQL语句的全表扫描 + +### 状态值,不容易使用到索引 + +如性别、支付状态等状态值字段往往只有极少的几种取值可能,这种字段即使建立索引,也往往利用不上。这是因为,一个状态值可能匹配大量的记录,这种情况MySQL会认为利用索引比全表扫描的效率低,从而弃用索引。索引是随机访问磁盘,而全表扫描是顺序访问磁盘,这就好比有一栋20层楼的写字楼,楼底下的索引牌上写着某个公司对应不相邻的几层楼,你去公司找人,与其按照索引牌的提示去其中一层楼没找到再下来看索引牌再上楼,不如从1楼挨个往上找到顶楼。 + +## 如何创建索引 + +- 建立基础索引:在`where、order by、join`字段上建立索引。 +- 优化,组合索引:基于业务逻辑 + - 如果条件经常性出现在一起,那么可以考虑将多字段索引升级为==复合索引== + - 如果通过增加个别字段的索引,就可以出现==索引覆盖==,那么可以考虑为该字段建立索引 + - 查询时,不常用到的索引,应该删除掉 + +## 前缀索引 + +语法:`index(field(10))`,使用字段值的前10个字符建立索引,默认是使用字段的全部内容建立索引。 + +前提:前缀的标识度高。比如密码就适合建立前缀索引,因为密码几乎各不相同。 + +==实操的难度==:在于前缀截取的长度。 + +我们可以利用`select count(*)/count(distinct left(password,prefixLen));`,通过从调整`prefixLen`的值(从1自增)查看不同前缀长度的一个平均匹配度,接近1时就可以了(表示一个密码的前`prefixLen`个字符几乎能确定唯一一条记录) + +## 索引的存储结构 + +### BTree + +btree(多路平衡查找树)是一种广泛应用于==磁盘上实现索引功能==的一种数据结构,也是大多数数据库索引表的实现。 + +以`add index(first_name,last_name)`为例: + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/14728955.jpg) + +BTree的一个node可以存储多个关键字,node的大小取决于计算机的文件系统,因此我们可以通过减小索引字段的长度使结点存储更多的关键字。如果node中的关键字已满,那么可以通过每个关键字之间的子节点指针来拓展索引表,但是不能破坏结构的有序性,比如按照`first_name`第一有序、`last_name`第二有序的规则,新添加的`韩香`就可以插到`韩康`之后。`白起 < 韩飞 < 韩康 < 李世民 < 赵奢 < 李寻欢 < 王语嫣 < 杨不悔`。这与二叉搜索树的思想是一样的,只不过二叉搜索树的查找效率是`log(2,N)`(以2为底N的对数),而BTree的查找效率是`log(x,N)`(其中x为node的关键字数量,可以达到1000以上)。 + +从`log(1000+,N)`可以看出,少量的磁盘读取即可做到大量数据的遍历,这也是btree的设计目的。 + +### B+Tree聚簇结构 + +聚簇结构(也是在BTree上升级改造的)中,关键字和记录是存放在一起的。 + +在MySQL中,仅仅只有`Innodb`的==主键索引为聚簇结构==,其它的索引包括`Innodb`的非主键索引都是典型的BTree结构。 + +### 哈希索引 + +在索引被载入内存时,使用哈希结构来存储。 + +# 查询缓存 + +> 缓存`select`语句的查询结果 + +## 在配置文件中开启缓存 + +windows上是`my.ini`,linux上是`my.cnf` + +在`[mysqld]`段中配置`query_cache_type`: + +- 0:不开启 +- 1:开启,默认缓存所有,需要在SQL语句中增加`select sql-no-cache`提示来放弃缓存 +- 2:开启,默认都不缓存,需要在SQL语句中增加`select sql-cache`来主动缓存(==常用==) + +更改配置后需要重启以使配置生效,重启后可通过`show variables like ‘query_cache_type’;`来查看: + +```sql +show variables like 'query_cache_type'; +query_cache_type DEMAND +``` + +## 在客户端设置缓存大小 + +通过配置项`query_cache_size`来设置: + +```sql +show variables like 'query_cache_size'; +query_cache_size 0 + +set global query_cache_size=64*1024*1024; +show variables like 'query_cache_size'; +query_cache_size 67108864 +``` + +## 将查询结果缓存 + +`select sql_cache * from user;` + +## 重置缓存 + +`reset query cache;` + +## 缓存失效问题(大问题) + +当数据表改动时,基于该数据表的任何缓存都会被删除。(表层面的管理,不是记录层面的管理,因此失效率较高) + +## 注意事项 + +1. 应用程序,不应该关心`query cache`的使用情况。可以尝试使用,但不能由`query cache`决定业务逻辑,因为`query cache`由DBA来管理。 +2. 缓存是以SQL语句为key存储的,因此即使SQL语句功能相同,但如果多了一个空格或者大小写有差异都会导致匹配不到缓存。 + +# 分区 + +一般情况下我们创建的表对应一组存储文件,使用`MyISAM`存储引擎时是一个`.MYI`和`.MYD`文件,使用`Innodb`存储引擎时是一个`.ibd`和`.frm`(表结构)文件。 + +当数据量较大时(一般千万条记录级别以上),MySQL的性能就会开始下降,这时我们就需要将数据分散到多组存储文件,==保证其单个文件的执行效率==。 + +最常见的分区方案是按`id`分区,如下将`id`的哈希值对10取模将数据均匀分散到10个`.ibd`存储文件中: + +```sql +create table article( + id int auto_increment PRIMARY KEY, + title varchar(64), + content text +)PARTITION by HASH(id) PARTITIONS 10 +``` + +查看`data`目录: + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/92311249.jpg) + +> ==服务端的表分区对于客户端是透明的==,客户端还是照常插入数据,但服务端会按照分区算法分散存储数据。 + +## MySQL提供的分区算法 + +> ==分区依据的字段必须是主键的一部分==,分区是为了快速定位数据,因此该字段的搜索频次较高应作为强检索字段,否则依照该字段分区毫无意义 + +### hash(field) + +相同的输入得到相同的输出。输出的结果跟输入是否具有规律无关。==仅适用于整型字段== + +### key(field) + +和`hash(field)`的性质一样,只不过`key`是==处理字符串==的,比`hash()`多了一步从字符串中计算出一个整型在做取模操作。 + +```sql +create table article_key( + id int auto_increment, + title varchar(64), + content text, + PRIMARY KEY (id,title) -- 要求分区依据字段必须是主键的一部分 +)PARTITION by KEY(title) PARTITIONS 10 +``` + +### range算法 + +是一种==条件分区==算法,按照数据大小范围分区(将数据使用某种条件,分散到不同的分区中)。 + +如下,按文章的发布时间将数据按照2018年8月、9月、10月分区存放: + +```sql +create table article_range( + id int auto_increment, + title varchar(64), + content text, + created_time int, -- 发布时间到1970-1-1的毫秒数 + PRIMARY KEY (id,created_time) -- 要求分区依据字段必须是主键的一部分 +)charset=utf8 +PARTITION BY RANGE(created_time)( + PARTITION p201808 VALUES less than (1535731199), -- select UNIX_TIMESTAMP('2018-8-31 23:59:59') + PARTITION p201809 VALUES less than (1538323199), -- 2018-9-30 23:59:59 + PARTITION p201810 VALUES less than (1541001599) -- 2018-10-31 23:59:59 +); +``` + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/89312692.jpg) + +注意:条件运算符只能使用==less than==,这以为着较小的范围要放在前面,比如上述`p201808,p201819,p201810`分区的定义顺序依照`created_time`数值范围从小到大,不能颠倒。 + +```sql +insert into article_range values(null,'MySQL优化','内容示例',1535731180); +flush tables; -- 使操作立即刷新到磁盘文件 +``` + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/54910879.jpg) + +由于插入的文章的发布时间`1535731180`小于`1535731199`(`2018-8-31 23:59:59`),因此被存储到`p201808`分区中,这种算法的存储到哪个分区取决于数据状况。 + +### list算法 + +也是一种条件分区,按照列表值分区(`in (值列表)`)。 + +```sql +create table article_list( + id int auto_increment, + title varchar(64), + content text, + status TINYINT(1), -- 文章状态:0-草稿,1-完成但未发布,2-已发布 + PRIMARY KEY (id,status) -- 要求分区依据字段必须是主键的一部分 +)charset=utf8 +PARTITION BY list(status)( + PARTITION writing values in(0,1), -- 未发布的放在一个分区 + PARTITION published values in (2) -- 已发布的放在一个分区 +); +``` + +```sql +insert into article_list values(null,'mysql优化','内容示例',0); +flush tables; +``` + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/54240876.jpg) + +## 分区管理语法 + +### range/list + +#### 增加分区 + +前文中我们尝试使用`range`对文章按照月份归档,随着时间的增加,我们需要增加一个月份: + +```sql +alter table article_range add partition( + partition p201811 values less than (1543593599) -- select UNIX_TIMESTAMP('2018-11-30 23:59:59') + -- more +); +``` + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/17160012.jpg) + +#### 删除分区 + +```sql +alter table article_range drop PARTITION p201808 +``` + +注意:==删除分区后,分区中原有的数据也会随之删除!== + +### key/hash + +#### 新增分区 + +```sql +alter table article_key add partition partitions 4 +``` + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-26/83089746.jpg) + +#### 销毁分区 + +```sql +alter table article_key coalesce partition 6 +``` + +`key/hash`分区的管理不会删除数据,但是每一次调整(新增或销毁分区)都会将所有的数据重写分配到新的分区上。==效率极低==,最好在设计阶段就考虑好分区策略。 + +## 分区的使用 + +当数据表中的数据量很大时,分区带来的效率提升才会显现出来。 + +只有检索字段为分区字段时,分区带来的效率提升才会比较明显。因此,==分区字段的选择很重要==,并且==业务逻辑要尽可能地根据分区字段做相应调整==(尽量使用分区字段作为查询条件)。 + +# 水平分割和垂直分割 + +> 水平分割:通过建立结构相同的几张表分别存储数据 +> +> 垂直分割:将经常一起使用的字段放在一个单独的表中,分割后的表记录之间是一一对应关系。 + +## 分表原因 + +- 为数据库减压 +- 分区算法局限 +- 数据库支持不完善(`5.1`之后`mysql`才支持分区操作) + +## id重复的解决方案 + +- 借用第三方应用如`memcache、redis`的`id`自增器 +- 单独建一张只包含`id`一个字段的表,每次自增该字段作为数据记录的`id` + +# 集群 + +> 横向扩展:从根本上(单机的硬件处理能力有限)提升数据库性能 。由此而生的相关技术:==读写分离、负载均衡== + +## 安装和配置主从复制 + +### 环境 + +- `Red Hat Enterprise Linux Server release 7.0 (Maipo)`(虚拟机) +- `mysql5.7`([下载地址](https://mirrors.163.com/mysql/Downloads/MySQL-5.7/mysql-5.7.23-linux-glibc2.12-x86_64.tar.gz)) + +### 安装和配置 + +解压到对外提供的服务的目录(我自己专门创建了一个`/export/server`来存放) + +```shell +tar xzvf mysql-5.7.23-linux-glibc2.12-x86_64.tar.gz -C /export/server +cd /export/server +mv mysql-5.7.23-linux-glibc2.12-x86_64 mysql +``` + +添加`mysql`目录的所属组和所属者: + +```shell +groupadd mysql +useradd -r -g mysql mysql +cd /export/server +chown -R mysql:mysql mysql/ +chmod -R 755 mysql/ +``` + +创建`mysql`数据存放目录(其中`/export/data`是我创建专门用来为各种服务存放数据的目录) + +```shell +mkdir /export/data/mysql +``` + +初始化`mysql`服务 + +```shell +cd /export/server/mysql +./bin/mysqld --basedir=/export/server/mysql --datadir=/export/data/mysql --user=mysql --pid-file=/export/data/mysql/mysql.pid --initialize +``` + +> 如果成功会显示`mysql`的`root`账户的初始密码,记下来以备后续登录。如果报错缺少依赖,则使用`yum instally`依次安装即可 + +配置`my.cnf` + +```shell +vim /etc/my.cnf + +[mysqld] +basedir=/export/server/mysql +datadir=/export/data/mysql +socket=/tmp/mysql.sock +user=mysql +server-id=10 # 服务id,在集群时必须唯一,建议设置为IP的第四段 +port=3306 +# Disabling symbolic-links is recommended to prevent assorted security risks +symbolic-links=0 +# Settings user and group are ignored when systemd is used. +# If you need to run mysqld under a different user or group, +# customize your systemd unit file for mariadb according to the +# instructions in http://fedoraproject.org/wiki/Systemd + +[mysqld_safe] +log-error=/export/data/mysql/error.log +pid-file=/export/data/mysql/mysql.pid + +# +# include all files from the config directory +# +!includedir /etc/my.cnf.d +``` + +将服务添加到开机自动启动 + +```shell +cp /export/server/mysql/support-files/mysql.server /etc/init.d/mysqld +``` + +启动服务 + +```shell +service mysqld start +``` + +配置环境变量,在`/etc/profile`中添加如下内容 + +```shell +# mysql env +MYSQL_HOME=/export/server/mysql +MYSQL_PATH=$MYSQL_HOME/bin +PATH=$PATH:$MYSQL_PATH +export PATH +``` + +使配置即可生效 + +```shell +source /etc/profile +``` + +使用`root`登录 + +```shell +mysql -uroot -p +# 这里填写之前初始化服务时提供的密码 +``` + +登录上去之后,更改`root`账户密码(我为了方便将密码改为root),否则操作数据库会报错 + +```mysql +set password=password('root'); +flush privileges; +``` + +设置服务可被所有远程客户端访问 + +```mysql +use mysql; +update user set host='%' where user='root'; +flush privileges; +``` + +> 这样就可以在宿主机使用`navicat`远程连接虚拟机linux上的mysql了 + +### 配置主从节点 + +#### 配置master + +以`linux`(`192.168.10.10`)上的`mysql`为`master`,宿主机(`192.168.10.1`)上的`mysql`为`slave`配置主从复制。 + +修改`master`的`my.cnf`如下 + +```shell +[mysqld] +basedir=/export/server/mysql +datadir=/export/data/mysql +socket=/tmp/mysql.sock +user=mysql +server-id=10 +port=3306 +# Disabling symbolic-links is recommended to prevent assorted security risks +symbolic-links=0 +# Settings user and group are ignored when systemd is used. +# If you need to run mysqld under a different user or group, +# customize your systemd unit file for mariadb according to the +# instructions in http://fedoraproject.org/wiki/Systemd + +log-bin=mysql-bin # 开启二进制日志 +expire-logs-days=7 # 设置日志过期时间,避免占满磁盘 +binlog-ignore-db=mysql # 不使用主从复制的数据库 +binlog-ignore-db=information_schema +binlog-ignore-db=performation_schema +binlog-ignore-db=sys +binlog-do-db=test #使用主从复制的数据库 + +[mysqld_safe] +log-error=/export/data/mysql/error.log +pid-file=/export/data/mysql/mysql.pid + +# +# include all files from the config directory +# +!includedir /etc/my.cnf.d +``` + +重启`master` + +```shell +service mysqld restart +``` + +登录`master`查看配置是否生效(`ON`即为开启,默认为`OFF`): + +```mysql +mysql> show variables like 'log_bin'; ++---------------+-------+ +| Variable_name | Value | ++---------------+-------+ +| log_bin | ON | ++---------------+-------+ +``` + +在`master`的数据库中建立备份账号:`backup`为用户名,`%`表示任何远程地址,用户`back`可以使用密码`1234`通过任何远程客户端连接`master` + +```mysql +grant replication slave on *.* to 'backup'@'%' identified by '1234' +``` + +查看`user`表可以看到我们刚创建的用户: + +```sql +mysql> use mysql +mysql> select user,authentication_string,host from user; ++---------------+-------------------------------------------+-----------+ +| user | authentication_string | host | ++---------------+-------------------------------------------+-----------+ +| root | *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B | % | +| mysql.session | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | localhost | +| mysql.sys | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | localhost | +| backup | *A4B6157319038724E3560894F7F932C8886EBFCF | % | ++---------------+-------------------------------------------+-----------+ +``` + +新建`test`数据库,创建一个`article`表以备后续测试 + +```sql +CREATE TABLE `article` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `title` varchar(64) DEFAULT NULL, + `content` text, + PRIMARY KEY (`id`) +) CHARSET=utf8; +``` + +重启服务并刷新数据库状态到存储文件中(`with read lock`表示在此过程中,客户端只能读数据,以便获得一个一致性的快照) + +```shell +[root@zhenganwen ~]# service mysqld restart +Shutting down MySQL.... SUCCESS! +Starting MySQL. SUCCESS! +[root@zhenganwen mysql]# mysql -uroot -proot +mysql> flush tables with read lock; +Query OK, 0 rows affected (0.00 sec) +``` + +查看`master`上当前的二进制日志和偏移量(记一下其中的`File`和`Position`) + +```sql +mysql> show master status \G +*************************** 1. row *************************** + File: mysql-bin.000002 + Position: 154 + Binlog_Do_DB: test + Binlog_Ignore_DB: mysql,information_schema,performation_schema,sys +Executed_Gtid_Set: +1 row in set (0.00 sec) +``` + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-29/90485140.jpg) + +`File`表示实现复制功能的日志,即上图中的`Binary log`;`Position`则表示`Binary log`日志文件的偏移量之后的都会同步到`slave`中,那么在偏移量之前的则需要我们手动导入。 + +主服务器上面的任何修改都会保存在二进制日志Binary log里面,从服务器上面启动一个I/O thread(实际上就是一个主服务器的客户端进程),连接到主服务器上面请求读取二进制日志,然后把读取到的二进制日志写到本地的一个Realy log里面。从服务器上面开启一个SQL thread定时检查Realy log,如果发现有更改立即把更改的内容在本机上面执行一遍。 + +如果一主多从的话,这时主库既要负责写又要负责为几个从库提供二进制日志。此时可以稍做调整,将二进制日志只给某一从,这一从再开启二进制日志并将自己的二进制日志再发给其它从。或者是干脆这个从不记录只负责将二进制日志转发给其它从,这样架构起来性能可能要好得多,而且数据之间的延时应该也稍微要好一些 + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-29/37162547.jpg) + +手动导入,从`master`中导出数据 + +```shell +mysqldump -uroot -proot -hlocalhost test > /export/data/test.sql +``` + +将`test.sql`中的内容在`slave`上执行一遍。 + +#### 配置slave + +修改`slave`的`my.ini`文件中的`[mysqld]`部分 + +```ini +log-bin=mysql +server-id=1 #192.168.10.1 +``` + +保存修改后重启`slave`,`WIN+R`->`services.msc`->`MySQL5.7`->重新启动 + +登录`slave`检查`log_bin`是否以被开启: + +```sql +show VARIABLES like 'log_bin'; +``` + +配置与`master`的同步复制: + +```sql +stop slave; +change master to + master_host='192.168.10.10', -- master的IP + master_user='backup', -- 之前在master上创建的用户 + master_password='1234', + master_log_file='mysql-bin.000002', -- master上 show master status \G 提供的信息 + master_log_pos=154; +``` + +启用`slave`节点并查看状态 + +```sql +mysql> start slave; +mysql> show slave status \G +*************************** 1. row *************************** + Slave_IO_State: Waiting for master to send event + Master_Host: 192.168.10.10 + Master_User: backup + Master_Port: 3306 + Connect_Retry: 60 + Master_Log_File: mysql-bin.000002 + Read_Master_Log_Pos: 154 + Relay_Log_File: DESKTOP-KUBSPE0-relay-bin.000002 + Relay_Log_Pos: 320 + Relay_Master_Log_File: mysql-bin.000002 + Slave_IO_Running: Yes + Slave_SQL_Running: Yes + Replicate_Do_DB: + Replicate_Ignore_DB: + Replicate_Do_Table: + Replicate_Ignore_Table: + Replicate_Wild_Do_Table: + Replicate_Wild_Ignore_Table: + Last_Errno: 0 + Last_Error: + Skip_Counter: 0 + Exec_Master_Log_Pos: 154 + Relay_Log_Space: 537 + Until_Condition: None + Until_Log_File: + Until_Log_Pos: 0 + Master_SSL_Allowed: No + Master_SSL_CA_File: + Master_SSL_CA_Path: + Master_SSL_Cert: + Master_SSL_Cipher: + Master_SSL_Key: + Seconds_Behind_Master: 0 +Master_SSL_Verify_Server_Cert: No + Last_IO_Errno: 0 + Last_IO_Error: + Last_SQL_Errno: 0 + Last_SQL_Error: + Replicate_Ignore_Server_Ids: + Master_Server_Id: 10 + Master_UUID: f68774b7-0b28-11e9-a925-000c290abe05 + Master_Info_File: C:\ProgramData\MySQL\MySQL Server 5.7\Data\master.info + SQL_Delay: 0 + SQL_Remaining_Delay: NULL + Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates + Master_Retry_Count: 86400 + Master_Bind: + Last_IO_Error_Timestamp: + Last_SQL_Error_Timestamp: + Master_SSL_Crl: + Master_SSL_Crlpath: + Retrieved_Gtid_Set: + Executed_Gtid_Set: + Auto_Position: 0 + Replicate_Rewrite_DB: + Channel_Name: + Master_TLS_Version: +1 row in set (0.00 sec) +``` + +> 注意查看第4、14、15三行,若与我一致,表示`slave`配置成功 + +### 测试 + +关闭`master`的读取锁定 + +```sql +mysql> unlock tables; +Query OK, 0 rows affected (0.00 sec) +``` + +向`master`中插入一条数据 + +```sql +mysql> use test +mysql> insert into article (title,content) values ('mysql master and slave','record the cluster building succeed!:)'); +Query OK, 1 row affected (0.00 sec) +``` + +查看`slave`是否自动同步了数据 + +```sql +mysql> insert into article (title,content) values ('mysql master and slave','record the cluster building succeed!:)'); +Query OK, 1 row affected (0.00 sec) +``` + +至此,主从复制的配置成功!:) + +[使用mysqlreplicate命令快速搭建 Mysql 主从复制](https://blog.csdn.net/xlgen157387/article/details/52452394) + +## 读写分离 + +读写分离是依赖于主从复制,而主从复制又是为读写分离服务的。因为主从复制要求`slave`不能写只能读(如果对`slave`执行写操作,那么`show slave status`将会呈现`Slave_SQL_Running=NO`,此时你需要按照前面提到的手动同步一下`slave`)。 + +### 方案一、定义两种连接 + +就像我们在学JDBC时定义的`DataBase`一样,我们可以抽取出`ReadDataBase,WriteDataBase implements DataBase`,但是这种方式无法利用优秀的线程池技术如`DruidDataSource`帮我们管理连接,也无法利用`Spring AOP`让连接对`DAO`层透明。 + +### 方案二、使用Spring AOP + +如果能够使用`Spring AOP`解决数据源切换的问题,那么就可以和`Mybatis`、`Druid`整合到一起了。 + +我们在整合`Spring1`和`Mybatis`时,我们只需写DAO接口和对应的`SQL`语句,那么DAO实例是由谁创建的呢?实际上就是`Spring`帮我们创建的,它通过我们注入的数据源,帮我们完成从中获取数据库连接、使用连接执行 `SQL` 语句的过程以及最后归还连接给数据源的过程。 + +如果我们能在调用DAO接口时根据接口方法命名规范(增`addXXX/createXXX`、删`deleteXX/removeXXX`、改`updateXXXX`、查`selectXX/findXXX/getXX/queryXXX`)动态地选择数据源(读数据源对应连接`master`而写数据源对应连接`slave`),那么就可以做到读写分离了。 + +#### 项目结构 + +![](http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-29/71747137.jpg) + +#### 引入依赖 + +其中,为了方便访问数据库引入了`mybatis`和`druid`,实现数据源动态切换主要依赖`spring-aop`和`spring-aspects` + +```xml + + + org.mybatis + mybatis-spring + 1.3.2 + + + org.mybatis + mybatis + 3.4.6 + + + org.springframework + spring-core + 5.0.8.RELEASE + + + org.springframework + spring-aop + 5.0.8.RELEASE + + + org.springframework + spring-jdbc + 5.0.8.RELEASE + + + com.alibaba + druid + 1.1.6 + + + mysql + mysql-connector-java + 6.0.2 + + + org.springframework + spring-context + 5.0.8.RELEASE + + + + org.springframework + spring-aspects + 5.0.8.RELEASE + + + + org.projectlombok + lombok + 1.16.22 + + + org.springframework + spring-test + 5.0.8.RELEASE + + + junit + junit + 4.12 + + + +``` + +#### 数据类 + +```java +package top.zhenganwen.mysqloptimize.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Article { + + private int id; + private String title; + private String content; +} +``` + +#### spring配置文件 + +其中`RoutingDataSourceImpl`是实现动态切换功能的核心类,稍后介绍。 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +`dp.properties` + +```properties +master.db.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC +master.db.username=root +master.db.password=root + +slave.db.url=jdbc:mysql://192.168.10.10:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC +slave.db.username=root +slave.db.password=root + +db.driverClass=com.mysql.jdbc.Driver +``` + +`mybatis-config.xml` + +```xml + + + + + + + +``` + +#### mapper接口和配置文件 + +`ArticleMapper.java` + +```java +package top.zhenganwen.mysqloptimize.mapper; + +import org.springframework.stereotype.Repository; +import top.zhenganwen.mysqloptimize.entity.Article; + +import java.util.List; + +@Repository +public interface ArticleMapper { + + List
findAll(); + + void add(Article article); + + void delete(int id); + +} +``` + +`ArticleMapper.xml` + +```xml + + + + + + + insert into article (title,content) values (#{title},#{content}) + + + + delete from article where id=#{id} + + +``` + +#### 核心类 + +##### RoutingDataSourceImpl + +```java +package top.zhenganwen.mysqloptimize.dataSource; + +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +import java.util.*; + +/** + * RoutingDataSourceImpl class + * 数据源路由 + * + * @author zhenganwen, blog:zhenganwen.top + * @date 2018/12/29 + */ +public class RoutingDataSourceImpl extends AbstractRoutingDataSource { + + /** + * key为read或write + * value为DAO方法的前缀 + * 什么前缀开头的方法使用读数据员,什么开头的方法使用写数据源 + */ + public static final Map> METHOD_TYPE_MAP = new HashMap>(); + + /** + * 由我们指定数据源的id,由Spring切换数据源 + * + * @return + */ + @Override + protected Object determineCurrentLookupKey() { + System.out.println("数据源为:"+DataSourceHandler.getDataSource()); + return DataSourceHandler.getDataSource(); + } + + public void setMethodType(Map map) { + for (String type : map.keySet()) { + String methodPrefixList = map.get(type); + if (methodPrefixList != null) { + METHOD_TYPE_MAP.put(type, Arrays.asList(methodPrefixList.split(","))); + } + } + } +} +``` + +它的主要功能是,本来我们只配置一个数据源,因此`Spring`动态代理DAO接口时直接使用该数据源,现在我们有了读、写两个数据源,我们需要加入一些自己的逻辑来告诉调用哪个接口使用哪个数据源(读数据的接口使用`slave`,写数据的接口使用`master`。这个告诉`Spring`该使用哪个数据源的类就是`AbstractRoutingDataSource`,必须重写的方法`determineCurrentLookupKey`返回数据源的标识,结合`spring`配置文件(下段代码的5,6两行) + +```xml + + + + + + + + + + + + + + + +``` + +如果`determineCurrentLookupKey`返回`read`那么使用`slaveDataSource`,如果返回`write`就使用`masterDataSource`。 + +##### DataSourceHandler + +```java +package top.zhenganwen.mysqloptimize.dataSource; + +/** + * DataSourceHandler class + *

+ * 将数据源与线程绑定,需要时根据线程获取 + * + * @author zhenganwen, blog:zhenganwen.top + * @date 2018/12/29 + */ +public class DataSourceHandler { + + /** + * 绑定的是read或write,表示使用读或写数据源 + */ + private static final ThreadLocal holder = new ThreadLocal(); + + public static void setDataSource(String dataSource) { + System.out.println(Thread.currentThread().getName()+"设置了数据源类型"); + holder.set(dataSource); + } + + public static String getDataSource() { + System.out.println(Thread.currentThread().getName()+"获取了数据源类型"); + return holder.get(); + } +} +``` + +##### DataSourceAspect + +```java +package top.zhenganwen.mysqloptimize.dataSource; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +import static top.zhenganwen.mysqloptimize.dataSource.RoutingDataSourceImpl.METHOD_TYPE_MAP; + +/** + * DataSourceAspect class + * + * 配置切面,根据方法前缀设置读、写数据源 + * 项目启动时会加载该bean,并按照配置的切面(哪些切入点、如何增强)确定动态代理逻辑 + * @author zhenganwen,blog:zhenganwen.top + * @date 2018/12/29 + */ +@Component +//声明这是一个切面,这样Spring才会做相应的配置,否则只会当做简单的bean注入 +@Aspect +@EnableAspectJAutoProxy +public class DataSourceAspect { + + /** + * 配置切入点:DAO包下的所有类的所有方法 + */ + @Pointcut("execution(* top.zhenganwen.mysqloptimize.mapper.*.*(..))") + public void aspect() { + + } + + /** + * 配置前置增强,对象是aspect()方法上配置的切入点 + */ + @Before("aspect()") + public void before(JoinPoint point) { + String className = point.getTarget().getClass().getName(); + String invokedMethod = point.getSignature().getName(); + System.out.println("对 "+className+"$"+invokedMethod+" 做了前置增强,确定了要使用的数据源类型"); + + Set dataSourceType = METHOD_TYPE_MAP.keySet(); + for (String type : dataSourceType) { + List prefixList = METHOD_TYPE_MAP.get(type); + for (String prefix : prefixList) { + if (invokedMethod.startsWith(prefix)) { + DataSourceHandler.setDataSource(type); + System.out.println("数据源为:"+type); + return; + } + } + } + } +} +``` + +#### 测试读写分离 + +> 如何测试读是从`slave`中读的呢?可以将写后复制到`slave`中的数据更改,再读该数据就知道是从`slave`中读了。==注意==,一但对`slave`做了写操作就要重新手动将`slave`与`master`同步一下,否则主从复制就会失效。 + +```java +package top.zhenganwen.mysqloptimize.dataSource; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import top.zhenganwen.mysqloptimize.entity.Article; +import top.zhenganwen.mysqloptimize.mapper.ArticleMapper; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = "classpath:spring-mybatis.xml") +public class RoutingDataSourceTest { + + @Autowired + ArticleMapper articleMapper; + + @Test + public void testRead() { + System.out.println(articleMapper.findAll()); + } + + @Test + public void testAdd() { + Article article = new Article(0, "我是新插入的文章", "测试是否能够写到master并且复制到slave中"); + articleMapper.add(article); + } + + @Test + public void testDelete() { + articleMapper.delete(2); + } +} +``` + +## 负载均衡 + +### 负载均衡算法 + +- 轮询 +- 加权轮询:按照处理能力来加权 +- 负载分配:依据当前的空闲状态(但是测试每个节点的内存使用率、CPU利用率等,再做比较选出最闲的那个,效率太低) + +## 高可用 + +在服务器架构时,为了保证服务器7x24不宕机在线状态,需要为每台单点服务器(由一台服务器提供服务的服务器,如写服务器、数据库中间件)提供冗余机。 + +对于写服务器来说,需要提供一台同样的写-冗余服务器,当写服务器健康时(写-冗余通过心跳检测),写-冗余作为一个从机的角色复制写服务器的内容与其做一个同步;当写服务器宕机时,写-冗余服务器便顶上来作为写服务器继续提供服务。对外界来说这个处理过程是透明的,即外界仅通过一个IP访问服务。 + +# 典型SQL + +## 线上DDL + +DDL(Database Definition Language)是指数据库表结构的定义(`create table`)和维护(`alter table`)的语言。在线上执行DDL,在低于`MySQL5.6`版本时会导致全表被独占锁定,此时表处于维护、不可操作状态,这会导致该期间对该表的所有访问无法响应。但是在`MySQL5.6`之后,支持`Online DDL`,大大缩短了锁定时间。 + +优化技巧是采用的维护表结构的DDL(比如增加一列,或者增加一个索引),是==copy==策略。思路:创建一个满足新结构的新表,将旧表数据==逐条==导入(复制)到新表中,以保证==一次性锁定的内容少==(锁定的是正在导入的数据),同时旧表上可以执行其他任务。导入的过程中,将对旧表的所有操作以日志的形式记录下来,导入完毕后,将更新日志在新表上再执行一遍(确保一致性)。最后,新表替换旧表(在应用程序中完成,或者是数据库的rename,视图完成)。 + +但随着MySQL的升级,这个问题几乎淡化了。 + +## 数据库导入语句 + +在恢复数据时,可能会导入大量的数据。此时为了快速导入,需要掌握一些技巧: + +1. 导入时==先禁用索引和约束==: + +```sql +alter table table-name disable keys +``` + +待数据导入完成之后,再开启索引和约束,一次性创建索引 + +```sql +alter table table-name enable keys +``` + +2. 数据库如果使用的引擎是`Innodb`,那么它==默认会给每条写指令加上事务==(这也会消耗一定的时间),因此建议先手动开启事务,再执行一定量的批量导入,最后手动提交事务。 +3. 如果批量导入的SQL指令格式相同只是数据不同,那么你应该先`prepare`==预编译==一下,这样也能节省很多重复编译的时间。 + +## limit offset,rows + +尽量保证不要出现大的`offset`,比如`limit 10000,10`相当于对已查询出来的行数弃掉前`10000`行后再取`10`行,完全可以加一些条件过滤一下(完成筛选),而不应该使用`limit`跳过已查询到的数据。这是一个==`offset`做无用功==的问题。对应实际工程中,要避免出现大页码的情况,尽量引导用户做条件过滤。 + +## select * 要少用 + +即尽量选择自己需要的字段`select`,但这个影响不是很大,因为网络传输多了几十上百字节也没多少延时,并且现在流行的ORM框架都是用的`select *`,只是我们在设计表的时候注意将大数据量的字段分离,比如商品详情可以单独抽离出一张商品详情表,这样在查看商品简略页面时的加载速度就不会有影响了。 + +## order by rand()不要用 + +它的逻辑就是随机排序(为每条数据生成一个随机数,然后根据随机数大小进行排序)。如`select * from student order by rand() limit 5`的执行效率就很低,因为它为表中的每条数据都生成随机数并进行排序,而我们只要前5条。 + +解决思路:在应用程序中,将随机的主键生成好,去数据库中利用主键检索。 + +## 单表和多表查询 + +多表查询:`join`、子查询都是涉及到多表的查询。如果你使用`explain`分析执行计划你会发现多表查询也是一个表一个表的处理,最后合并结果。因此可以说单表查询将计算压力放在了应用程序上,而多表查询将计算压力放在了数据库上。 + +现在有ORM框架帮我们解决了单表查询带来的对象映射问题(查询单表时,如果发现有外键自动再去查询关联表,是一个表一个表查的)。 + +## count(*) + +在`MyISAM`存储引擎中,会自动记录表的行数,因此使用`count(*)`能够快速返回。而`Innodb`内部没有这样一个计数器,需要我们手动统计记录数量,解决思路就是单独使用一张表: + +| id | table | count | +| ---- | ------- | ----- | +| 1 | student | 100 | + +## limit 1 + +如果可以确定仅仅检索一条,建议加上`limit 1`,其实ORM框架帮我们做到了这一点(查询单条的操作都会自动加上`limit 1`)。 + +# 慢查询日志 + +> 用于记录执行时间超过某个临界值的SQL日志,用于快速定位慢查询,为我们的优化做参考。 + +## 开启慢查询日志 + +配置项:`slow_query_log` + +可以使用`show variables like ‘slov_query_log’`查看是否开启,如果状态值为`OFF`,可以使用`set GLOBAL slow_query_log = on`来开启,它会在`datadir`下产生一个`xxx-slow.log`的文件。 + +## 设置临界时间 + +配置项:`long_query_time` + +查看:`show VARIABLES like 'long_query_time'`,单位秒 + +设置:`set long_query_time=0.5` + +实操时应该从长时间设置到短的时间,即将最慢的SQL优化掉 + +## 查看日志 + +一旦SQL超过了我们设置的临界时间就会被记录到`xxx-slow.log`中 + +# profile信息 + +配置项:`profiling` + +## 开启profile + +`set profiling=on` + +开启后,所有的SQL执行的详细信息都会被自动记录下来 + +```sql +mysql> show variables like 'profiling'; ++---------------+-------+ +| Variable_name | Value | ++---------------+-------+ +| profiling | OFF | ++---------------+-------+ +1 row in set, 1 warning (0.00 sec) + +mysql> set profiling=on; +Query OK, 0 rows affected, 1 warning (0.00 sec) +``` + +## 查看profile信息 + +`show profiles` + +```sql +mysql> show variables like 'profiling'; ++---------------+-------+ +| Variable_name | Value | ++---------------+-------+ +| profiling | ON | ++---------------+-------+ +1 row in set, 1 warning (0.00 sec) + +mysql> insert into article values (null,'test profile',':)'); +Query OK, 1 row affected (0.15 sec) + +mysql> show profiles; ++----------+------------+-------------------------------------------------------+ +| Query_ID | Duration | Query | ++----------+------------+-------------------------------------------------------+ +| 1 | 0.00086150 | show variables like 'profiling' | +| 2 | 0.15027550 | insert into article values (null,'test profile',':)') | ++----------+------------+-------------------------------------------------------+ +``` + +## 通过Query_ID查看某条SQL所有详细步骤的时间 + +`show profile for query Query_ID` + +上面`show profiles`的结果中,每个SQL有一个`Query_ID`,可以通过它查看执行该SQL经过了哪些步骤,各消耗了多场时间 + +```sql + +``` + +# 典型的服务器配置 + +> 以下的配置全都取决于实际的运行环境 + +- `max_connections`,最大客户端连接数 + + ```sql + mysql> show variables like 'max_connections'; + +-----------------+-------+ + | Variable_name | Value | + +-----------------+-------+ + | max_connections | 151 | + +-----------------+-------+ + ``` + +- `table_open_cache`,表文件句柄缓存(表数据是存储在磁盘上的,缓存磁盘文件的句柄方便打开文件读取数据) + + ```sql + mysql> show variables like 'table_open_cache'; + +------------------+-------+ + | Variable_name | Value | + +------------------+-------+ + | table_open_cache | 2000 | + +------------------+-------+ + ``` + +- `key_buffer_size`,索引缓存大小(将从磁盘上读取的索引缓存到内存,可以设置大一些,有利于快速检索) + + ```sql + mysql> show variables like 'key_buffer_size'; + +-----------------+---------+ + | Variable_name | Value | + +-----------------+---------+ + | key_buffer_size | 8388608 | + +-----------------+---------+ + ``` + +- `innodb_buffer_pool_size`,`Innodb`存储引擎缓存池大小(对于`Innodb`来说最重要的一个配置,如果所有的表用的都是`Innodb`,那么甚至建议将该值设置到物理内存的80%,`Innodb`的很多性能提升如索引都是依靠这个) + + ```sql + mysql> show variables like 'innodb_buffer_pool_size'; + +-------------------------+---------+ + | Variable_name | Value | + +-------------------------+---------+ + | innodb_buffer_pool_size | 8388608 | + +-------------------------+---------+ + ``` + +- `innodb_file_per_table`(`innodb`中,表数据存放在`.ibd`文件中,如果将该配置项设置为`ON`,那么一个表对应一个`ibd`文件,否则所有`innodb`共享表空间) + +# 压测工具mysqlslap + +安装MySQL时附带了一个压力测试工具`mysqlslap`(位于`bin`目录下) + +## 自动生成sql测试 + +```shell +C:\Users\zaw>mysqlslap --auto-generate-sql -uroot -proot +mysqlslap: [Warning] Using a password on the command line interface can be insecure. +Benchmark + Average number of seconds to run all queries: 1.219 seconds + Minimum number of seconds to run all queries: 1.219 seconds + Maximum number of seconds to run all queries: 1.219 seconds + Number of clients running queries: 1 + Average number of queries per client: 0 +``` + +## 并发测试 + +```shell +C:\Users\zaw>mysqlslap --auto-generate-sql --concurrency=100 -uroot -proot +mysqlslap: [Warning] Using a password on the command line interface can be insecure. +Benchmark + Average number of seconds to run all queries: 3.578 seconds + Minimum number of seconds to run all queries: 3.578 seconds + Maximum number of seconds to run all queries: 3.578 seconds + Number of clients running queries: 100 + Average number of queries per client: 0 + +C:\Users\zaw>mysqlslap --auto-generate-sql --concurrency=150 -uroot -proot +mysqlslap: [Warning] Using a password on the command line interface can be insecure. +Benchmark + Average number of seconds to run all queries: 5.718 seconds + Minimum number of seconds to run all queries: 5.718 seconds + Maximum number of seconds to run all queries: 5.718 seconds + Number of clients running queries: 150 + Average number of queries per client: 0 +``` + +## 多轮测试 + +```shell +C:\Users\zaw>mysqlslap --auto-generate-sql --concurrency=150 --iterations=10 -uroot -proot +mysqlslap: [Warning] Using a password on the command line interface can be insecure. +Benchmark + Average number of seconds to run all queries: 5.398 seconds + Minimum number of seconds to run all queries: 4.313 seconds + Maximum number of seconds to run all queries: 6.265 seconds + Number of clients running queries: 150 + Average number of queries per client: 0 +``` + +## 存储引擎测试 + +```shell +C:\Users\zaw>mysqlslap --auto-generate-sql --concurrency=150 --iterations=3 --engine=innodb -uroot -proot +mysqlslap: [Warning] Using a password on the command line interface can be insecure. +Benchmark + Running for engine innodb + Average number of seconds to run all queries: 5.911 seconds + Minimum number of seconds to run all queries: 5.485 seconds + Maximum number of seconds to run all queries: 6.703 seconds + Number of clients running queries: 150 + Average number of queries per client: 0 +``` + +```shell +C:\Users\zaw>mysqlslap --auto-generate-sql --concurrency=150 --iterations=3 --engine=myisam -uroot -proot +mysqlslap: [Warning] Using a password on the command line interface can be insecure. +Benchmark + Running for engine myisam + Average number of seconds to run all queries: 53.104 seconds + Minimum number of seconds to run all queries: 46.843 seconds + Maximum number of seconds to run all queries: 60.781 seconds + Number of clients running queries: 150 + Average number of queries per client: 0 +``` + From 29654f7d4f00334c8be3a0a6f9dc3a744c2d5bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Mon, 12 Aug 2019 23:32:16 +0800 Subject: [PATCH 015/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...71\345\222\214\347\255\224\346\241\210.md" | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 22bc65f7..61da638d 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -211,22 +211,13 @@ String c = "xx" + "yy " + a + "zz" + "mm" + b; 实质上的实现过程是: St ``` # 1.18 Java 如何进行文件读取? -1.首先获得一个文件句柄。 File fille= new File(;file即为文件句柄。两人之间连 - +FileReader类是将文件按字符流的方式读取char数组或者String.FileInputStream则按字符流的方式读取文件byte数组。 +- 1.首先获得一个文件句柄。 File fille= new File();file即为文件句柄。两人之间连 通电话网络了。接下来可以开始打电话了 - - -2.通过这条线路读取甲方的信息: new FileInputStream(fe)目前这个信息已经 - +- 2.通过这条线路读取甲方的信息: new FileInputStream(fe)目前这个信息已经 读进来内存当中了。接下来需要解读成乙方可以理解的东西 - -3.既然你使用了 FileInputStream()。那么对应的需要使用 InputStream Reader( - +- 3.既然你使用了 FileInputStream()。那么对应的需要使用 InputStreamReader() 这个方法进行解读刚才装进来内存当中的数据 - - -4.解读完成后要输出呀。那当然要转换成ⅠO可以识别的数据呀。那就需要调用字节 - -码读取的方法 Bufferedreader()。同时使用 buffered Reader(的 readline()方 - +- 4.解读完成后要输出呀。那当然要转换成IO可以识别的数据呀。那就需要调用字节 +码读取的方法 Bufferedreader()。同时使用 bufferedReader()的 readline()方 法读取txt文件中的每一行数据哈。 From 26695248a63600c80f2612070e9465547c60e5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Mon, 12 Aug 2019 23:39:15 +0800 Subject: [PATCH 016/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 3 +++ 1 file changed, 3 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 61da638d..4d5cd948 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -221,3 +221,6 @@ FileReader类是将文件按字符流的方式读取char数组或者String.FileI - 4.解读完成后要输出呀。那当然要转换成IO可以识别的数据呀。那就需要调用字节 码读取的方法 Bufferedreader()。同时使用 bufferedReader()的 readline()方 法读取txt文件中的每一行数据哈。 +# 1.19 反射 +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5xbl9l6brj30xl0gygyf.jpg) +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5xblf6b2yj30lr0gb7dv.jpg) From fa82a83a3b83803b3975fb6856c7ce6a770db14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Mon, 12 Aug 2019 23:40:34 +0800 Subject: [PATCH 017/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 1 + 1 file changed, 1 insertion(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 4d5cd948..d0f327bd 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -223,4 +223,5 @@ FileReader类是将文件按字符流的方式读取char数组或者String.FileI 法读取txt文件中的每一行数据哈。 # 1.19 反射 ![](http://ww1.sinaimg.cn/large/007s8HJUly1g5xbl9l6brj30xl0gygyf.jpg) + ![](http://ww1.sinaimg.cn/large/007s8HJUly1g5xblf6b2yj30lr0gb7dv.jpg) From 304c3ede9c91925e382d25fa6ef2b095ba944f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Mon, 12 Aug 2019 23:49:33 +0800 Subject: [PATCH 018/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...271\345\222\214\347\255\224\346\241\210.md" | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index d0f327bd..566ce7b0 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -225,3 +225,21 @@ FileReader类是将文件按字符流的方式读取char数组或者String.FileI ![](http://ww1.sinaimg.cn/large/007s8HJUly1g5xbl9l6brj30xl0gygyf.jpg) ![](http://ww1.sinaimg.cn/large/007s8HJUly1g5xblf6b2yj30lr0gb7dv.jpg) + +反射机制中可以获取private成员的值吗(没有set和get函数) +# 1.19 JDK和JRE的区别? +- Java运行时环境(JRE)。它包括Java虚拟机、Java核心类库和支持文件。它不包含开发工具(JDK)--编译器、调试器和其他工具。 +- Java开发工具包(JDK)是完整的Java软件开发包,包含了JRE,编译器和其他的工具(比如:JavaDoc,Java调试器),可以让开发者开发、编译、执行Java应用程序。 + +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5xbpno6p2j311u0knjzt.jpg) +# 1.20 static和final +## (1) static和final区别 + +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5xbub06cdj30mn08lgot.jpg) + +## (2) final的好处: +1.final关键字提高了性能。JVM和Java应用都会缓存final变量。 +2.final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。 +3.使用final关键字,JVM会对方法、变量及类进行优化。 +## (3)static方法是否可以覆盖? +static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。 From 7bb48fd2ce96e18aff9945bb4900eb453443d80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Mon, 12 Aug 2019 23:59:56 +0800 Subject: [PATCH 019/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\202\271\345\222\214\347\255\224\346\241\210.md" | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 566ce7b0..de96bb01 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -243,3 +243,16 @@ FileReader类是将文件按字符流的方式读取char数组或者String.FileI 3.使用final关键字,JVM会对方法、变量及类进行优化。 ## (3)static方法是否可以覆盖? static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。 +# 1.21 session和cookie区别 + +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5xc3l5kikj30px075n0q.jpg) + +cookie是Web服务器发送给浏览器的一块信息。浏览器会在本地文件中给每一个Web服务器存储cookie。以后浏览器在给特定的Web服务器发请求的时候,同时会发送所有为该服务器存储的cookie。下面列出了session和cookie的区别: +无论客户端浏览器做怎么样的设置,session都应该能正常工作。客户端可以选择禁用cookie,但是,session仍然是能够工作的,因为客户端无法禁用服务端的session。 +在存储的数据量方面session和cookies也是不一样的。session能够存储任意的Java对象,cookie只能存储String类型的对象。 +服务器端Session的保存 +Cookie和session区别 +session在服务器上以怎样的形式存在session持久化 +怎么设置session和cookie的有效时间 +Session的实现原理和应用场景 Session原理; 既然Session是存储在服务器内存的, + From 328a4935e9388646da1791672d0ad3faaf95e13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Tue, 13 Aug 2019 00:00:41 +0800 Subject: [PATCH 020/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 1 - 1 file changed, 1 deletion(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index de96bb01..e98ee712 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -246,7 +246,6 @@ static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定 # 1.21 session和cookie区别 ![](http://ww1.sinaimg.cn/large/007s8HJUly1g5xc3l5kikj30px075n0q.jpg) - cookie是Web服务器发送给浏览器的一块信息。浏览器会在本地文件中给每一个Web服务器存储cookie。以后浏览器在给特定的Web服务器发请求的时候,同时会发送所有为该服务器存储的cookie。下面列出了session和cookie的区别: 无论客户端浏览器做怎么样的设置,session都应该能正常工作。客户端可以选择禁用cookie,但是,session仍然是能够工作的,因为客户端无法禁用服务端的session。 在存储的数据量方面session和cookies也是不一样的。session能够存储任意的Java对象,cookie只能存储String类型的对象。 From 58a5c76970a4ab91d47ea0c1e4bb36a5f2a78e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Tue, 13 Aug 2019 23:06:15 +0800 Subject: [PATCH 021/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...71\345\222\214\347\255\224\346\241\210.md" | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index e98ee712..6c5c27a7 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -255,3 +255,34 @@ session在服务器上以怎样的形式存在session持久化 怎么设置session和cookie的有效时间 Session的实现原理和应用场景 Session原理; 既然Session是存储在服务器内存的, +# 1.22 finalize finalization finally +## 一.finalize用途 +答:垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法 但是在Java中很不幸,如果内存总是充足的,那么垃圾回收可能永远不会进行,也就是说filalize()可能永远不被执行,显然指望它做收尾工作是靠不住的。 那么finalize()究竟是做什么的呢?它最主要的用途是回收特殊渠道申请的内存。Java程序有垃圾回收器,所以一般情况下内存问题不用程序员操心。但有一种JNI(Java Native Interface)调用non-Java程序(C或C++),finalize()的工作就是回收这部分的内存。 +## 二.finally +finally 一定会被执行,如果 finally 里有 return 语句,则覆盖 try/catch 里的 return , +比较爱考的是 finally 里没有 return 语句,这时虽然 finally 里对 return 的值进行了修改,但 return 的值并不改变这种情况 +## 三.finally代码块和finalize()方法有什么区别? +无论是否抛出异常,finally代码块都会执行,它主要是用来释放应用占用的资源。finalize()方法是Object类的一个protected方法,它是在对象被垃圾回收之前由Java虚拟机来调用的。 +# 1.23 public private default protected +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5yfwftsjrj30ha06wjsq.jpg) +不写时默认为default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。 +不可以覆盖private的方法,因为private修饰的变量和方法只能在当前类中使用,如果是其他的类继承当前类是不能访问到private变量或方法的,当然也不能覆盖 +# 1.24 Object有哪些方法? + +- hashcode() +- equals() +- toString() +- getClass() +- wait +- notify() +- notifyAll() +- finalize() + +# 1.25 equals和== + +- (1)对于==,如果作用于基本数据类型的变量(byte,short,char,int,long,float,double,boolean ),则直接比较其存储的"值"是否相等;如果作用于引用类型的变量(String),则比较的是所指向的对象的地址(即是否指向同一个对象)。 +- (2)equals方法是基类Object中的方法,因此对于所有的继承于Object的类都会有该方法。在Object类中,equals方法是用来比较两个对象的引用是否相等,即是否指向同一个对象。 +- (3)对于equals方法,注意:equals方法不能作用于基本数据类型的变量。如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;而String类对equals方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。其他的一些类诸如Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等。 +- (4)StringBuffer 和StringBuilder特殊,==和equal都是比较地址 + + From 36452813ad8956a9c862f6d9eae09b312935a9b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 14 Aug 2019 00:08:39 +0800 Subject: [PATCH 022/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...71\345\222\214\347\255\224\346\241\210.md" | 106 +++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 6c5c27a7..d9bd81a9 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -283,6 +283,110 @@ finally 一定会被执行,如果 finally 里有 return 语句,则覆盖 try - (1)对于==,如果作用于基本数据类型的变量(byte,short,char,int,long,float,double,boolean ),则直接比较其存储的"值"是否相等;如果作用于引用类型的变量(String),则比较的是所指向的对象的地址(即是否指向同一个对象)。 - (2)equals方法是基类Object中的方法,因此对于所有的继承于Object的类都会有该方法。在Object类中,equals方法是用来比较两个对象的引用是否相等,即是否指向同一个对象。 - (3)对于equals方法,注意:equals方法不能作用于基本数据类型的变量。如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;而String类对equals方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。其他的一些类诸如Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等。 -- (4)StringBuffer 和StringBuilder特殊,==和equal都是比较地址 +- (4)StringBuffer 和StringBuilder,==和equal都是比较地址 +# 1.26 异常 + +![](http://ww1.sinaimg.cn/large/007s8HJUly1g5yhdhue5xj30gk0j2n1r.jpg) + +Throwable.是ava话言中所有错误和异常的超类(万物即可抛).艺有两个子类:Eror、 Exception +异常种类 +- Err:Eror为错误,是程序无法处理的,如 Out OfMemoryεror、 Thread Death等,出现这种况你唯一能做的就 +是听之任之,交由M来处理,不过M在大多数情况下会选择终止程 +- Exception: Exception.是程序可以处理的异常。E又分为两种 CheckedException(受捡异常),一种是 +UncheckedException(不受检异常) + - CheckException发生在编译阶段,必须要使用ry- catch(或者 throws)否则编译不通过 + - UncheckedException发生在运行期,具有不确症性,主要是由于程序的逻辑引起的,难以排查,如除0的产生的异常,我们一般都需要纵观全局才能够发现这类的异常错误,所以在程序设计中我们需要认真考虐,好好写代码,尽量处理异常,即使产生了异常,也能尽量保证程序朝着有利方向发展,ClassCastException(类转换异常) IndexOutOfBoundsException(数组越界) NullPointerException(空指针) ArrayStoreException(数据存储异常,操作数组时类型不一致) +# 1.27 Runtime +Runtime:运行时,是一个封装了JVM的类。每一个JAVA程序实际上都是启动了一个JVM进程,每一个JVM进程都对应一个Runtime实例,此实例是由JVM为其实例化的。所以我们不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。一旦得到了一个当前的Runtime对象的引用,就可以调用Runtime对象的方法去控制Java虚拟机的状态和行为。 +查看官方文档可以看到,Runtime类中没有构造方法,本类的构造方法被私有化了, 所以才会有getRuntime方法返回本来的实例化对象,这与单例设计模式不谋而合 +public static Runtime getRuntime() +直接使用此静态方法可以取得Runtime类的实例 + +# 1.28 接口和抽象类 + +1.接口和抽象类的区别  +1,抽象类里可以有构造方法,而接口内不能有构造方法。 +2,抽象类中可以有普通成员变量,而接口中不能有普通成员变量。 +3,抽象类中可以包含非抽象的普通方法,而接口中所有的方法必须是抽象的,不能有非抽象的普通方法。 +4,抽象类中的抽象方法的访问类型可以是public ,protected和private,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。 +5,抽象类中可以包含静态方法,接口内不能包含静态方法。 +6,抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static类型,并且默认为public static final类型。 +7,一个类可以实现多个接口,但只能继承一个抽象类。 + +1. Java抽象类可以有构造函数吗? +可以有,抽象类可以声明并定义构造函数。因为你不可以创建抽象类的实例,所以构造函数只能通过构造函数链调用(Java中构造函数链指的是从其他构造函数调用一个构造函数),例如,当你创建具体的实现类。现在一些面试官问,如果你不能对抽象类实例化那么构造函数的作用是什么?好吧,它可以用来初始化抽象类内部声明的通用变量,并被各种实现使用。另外,即使你没有提供任何构造函数,编译器将为抽象类添加默认的无参数的构造函数,没有的话你的子类将无法编译,因为在任何构造函数中的第一条语句隐式调用super(),Java中默认超类的构造函数。 +2. Java抽象类可以实现接口吗?它们需要实现所有的方法吗? +可以,抽象类可以通过使用关键字implements来实现接口。因为它们是抽象的,所以它们不需要实现所有的方法。好的做法是,提供一个抽象基类以及一个接口来声明类型 。这样的例子是,java.util.List接口和相应的java.util.AbstractList抽象类。因为AbstractList实现了所有的通用方法,具体的实现像LinkedList和ArrayList不受实现所有方法的负担,它们可以直接实现List接口。这对两方面都很好,你可以利用接口声明类型的优点和抽象类的灵活性在一个地方实现共同的行为。Effective Java有个很好的章节,介绍如何使用Java的抽象类和接口,值得阅读。 +3. Java抽象类可以是final的吗? +不可以,Java抽象类不能是final的。将它们声明为final的将会阻止它们被继承,而这正是使用抽象类唯一的方法。它们也是彼此相反的,关键字abstract强制继承类,而关键字final阻止类被扩张。在现实世界中,抽象表示不完备性,而final是用来证明完整性。底线是,你不能让你的Java类既abstract又final,同时使用,是一个编译时错误。 +4. Java抽象类可以有static方法吗? +可以,抽象类可以声明并定义static方法,没什么阻止这样做。但是,你必须遵守Java中将方法声明为static的准则, + 5. 可以创建抽象类的实例吗? +不可以,你不能创建Java抽象类的实例,它们是不完全的。即使你的抽象类不包含任何抽象方法,你也不能对它实例化。将类声明为abstract的,就等你你告诉编译器,它是不完全的不应该被实例化。当一段代码尝试实例化一个抽象类时Java编译器会抛错误。 +6. 抽象类必须有抽象方法吗? +不需要,抽象类有抽象方法不是强制性的。你只需要使用关键字abstract就可以将类声明为抽象类。编译器会强制所有结构的限制来适用于抽象类,例如,现在允许创建一些实例。是否在抽象类中有抽象方法是引起争论的。我的观点是,抽象类应该有抽象方法,因为这是当程序员看到那个类并做假设的第一件事。这也符合最小惊奇原则。 +8. 何时选用抽象类而不是接口? +这是对之前抽象类和接口对比问题的后续。如果你知道语法差异,你可以很容易回答这个问题,因为它们可以令你做出抉择。当关心升级时,因为不可能在一个发布的接口中添加一个新方法,用抽象类会更好。类似地,如果你的接口中有很多方法,你对它们的实现感到很头疼,考虑提供一个抽象类作为默认实现。这是Java集合包中的模式,你可以使用提供默认实现List接口的AbstractList。 +9. Java中的抽象方法是什么? +抽象方法是一个没有方法体的方法。你仅需要声明一个方法,不需要定义它并使用关键字abstract声明。Java接口中所有方法的声明默认是abstract的。这是抽象方法的例子 +public void abstract printVersion(); +现在,为了实现这个方法,你需要继承该抽象类并重载这个方法。 +10. Java抽象类中可以包含main方法吗? +是的,抽象类可以包含main方法,它只是一个静态方法,你可以使用main方法执行抽象类,但不可以创建任何实例。 + +# 1.29 注解 +https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html +# 1.30 泛型 +http://www.importnew.com/24029.html +1. Java中的泛型是什么 ? 使用泛型的好处是什么? + +  这是在各种Java泛型面试中,一开场你就会被问到的问题中的一个,主要集中在初级和中级面试中。那些拥有Java1.4或更早版本的开发背景的人都知道,在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。 + +2. Java的泛型是如何工作的 ? 什么是类型擦除 ? + +  这是一道更好的泛型面试题。泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。根据你对这个泛型问题的回答情况,你会得到一些后续提问,比如为什么泛型是由类型擦除来实现的或者给你展示一些会导致编译器出错的错误泛型代码。请阅读我的Java中泛型是如何工作的来了解更多信息。 + +3. 什么是泛型中的限定通配符和非限定通配符 ? + +  这是另一个非常流行的Java泛型面试题。限定通配符对类型进行了限制。有两种限定通配符,一种是它通过确保类型必须是T的子类来设定类型的上界,另一种是它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面表示了非限定通配符,因为可以用任意类型来替代。更多信息请参阅我的文章泛型中限定通配符和非限定通配符之间的区别。 + +4. List和List 之间有什么区别 ? + +  这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List可以接受任何继承自T的类型的List,而List可以接受任何T的父类构成的List。例如List可以接受List或List。在本段出现的连接中可以找到更多信息。 + +5. 如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型? + +  编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样: +``` + public V put(K key, V value) { + + return cache.put(key, value); + + } +``` + 6. Java中如何使用泛型编写带有参数的类? + +  这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。 + +7. 编写一段泛型程序来实现LRU缓存? + +  对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。 + +8. 你可以把List传递给一个接受List参数的方法吗? + +  对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List应当可以用在需要List的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List可以存储任何类型的对象包括String, Integer等等,而List却只能用来存储Strings。  +``` + List objectList; + List stringList; + objectList = stringList; //compilation error incompatible types +``` +9. Array中可以用泛型吗? + +  这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。 + +  10. 如何阻止Java中的类型未检查的警告? + +  如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如   + List rawList = new ArrayList() From 2079f09d6b18f0e304b73ba7afaa7f6e1e31d429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 16 Aug 2019 23:02:14 +0800 Subject: [PATCH 023/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...71\345\222\214\347\255\224\346\241\210.md" | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index d9bd81a9..f4e88500 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -338,23 +338,23 @@ public void abstract printVersion(); https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html # 1.30 泛型 http://www.importnew.com/24029.html -1. Java中的泛型是什么 ? 使用泛型的好处是什么? +- 1. Java中的泛型是什么 ? 使用泛型的好处是什么? -  这是在各种Java泛型面试中,一开场你就会被问到的问题中的一个,主要集中在初级和中级面试中。那些拥有Java1.4或更早版本的开发背景的人都知道,在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。 + 这是在各种Java泛型面试中,一开场你就会被问到的问题中的一个,主要集中在初级和中级面试中。那些拥有Java1.4或更早版本的开发背景的人都知道,在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。 -2. Java的泛型是如何工作的 ? 什么是类型擦除 ? +- 2. Java的泛型是如何工作的 ? 什么是类型擦除 ?   这是一道更好的泛型面试题。泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。根据你对这个泛型问题的回答情况,你会得到一些后续提问,比如为什么泛型是由类型擦除来实现的或者给你展示一些会导致编译器出错的错误泛型代码。请阅读我的Java中泛型是如何工作的来了解更多信息。 -3. 什么是泛型中的限定通配符和非限定通配符 ? +- 3. 什么是泛型中的限定通配符和非限定通配符 ?   这是另一个非常流行的Java泛型面试题。限定通配符对类型进行了限制。有两种限定通配符,一种是它通过确保类型必须是T的子类来设定类型的上界,另一种是它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面表示了非限定通配符,因为可以用任意类型来替代。更多信息请参阅我的文章泛型中限定通配符和非限定通配符之间的区别。 -4. List和List 之间有什么区别 ? +- 4. List和List 之间有什么区别 ?   这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List可以接受任何继承自T的类型的List,而List可以接受任何T的父类构成的List。例如List可以接受List或List。在本段出现的连接中可以找到更多信息。 -5. 如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型? +- 5. 如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?   编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样: ``` @@ -364,15 +364,15 @@ http://www.importnew.com/24029.html } ``` - 6. Java中如何使用泛型编写带有参数的类? +- 6. Java中如何使用泛型编写带有参数的类?   这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。 -7. 编写一段泛型程序来实现LRU缓存? +- 7. 编写一段泛型程序来实现LRU缓存?   对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。 -8. 你可以把List传递给一个接受List参数的方法吗? +- 8. 你可以把List传递给一个接受List参数的方法吗?   对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List应当可以用在需要List的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List可以存储任何类型的对象包括String, Integer等等,而List却只能用来存储Strings。  ``` @@ -380,11 +380,11 @@ http://www.importnew.com/24029.html List stringList; objectList = stringList; //compilation error incompatible types ``` -9. Array中可以用泛型吗? +- 9. Array中可以用泛型吗?   这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。 -  10. 如何阻止Java中的类型未检查的警告? +- 10. 如何阻止Java中的类型未检查的警告?   如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如   From dcee11bae3bef8188b7988da97ed7d9774e3c187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 17 Aug 2019 00:05:39 +0800 Subject: [PATCH 024/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...71\345\222\214\347\255\224\346\241\210.md" | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index f4e88500..50f6c434 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -339,24 +339,16 @@ https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html # 1.30 泛型 http://www.importnew.com/24029.html - 1. Java中的泛型是什么 ? 使用泛型的好处是什么? - - 这是在各种Java泛型面试中,一开场你就会被问到的问题中的一个,主要集中在初级和中级面试中。那些拥有Java1.4或更早版本的开发背景的人都知道,在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。 - +这是在各种Java泛型面试中,一开场你就会被问到的问题中的一个,主要集中在初级和中级面试中。那些拥有Java1.4或更早版本的开发背景的人都知道,在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。 - 2. Java的泛型是如何工作的 ? 什么是类型擦除 ? - -  这是一道更好的泛型面试题。泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。根据你对这个泛型问题的回答情况,你会得到一些后续提问,比如为什么泛型是由类型擦除来实现的或者给你展示一些会导致编译器出错的错误泛型代码。请阅读我的Java中泛型是如何工作的来了解更多信息。 +这是一道更好的泛型面试题。泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。根据你对这个泛型问题的回答情况,你会得到一些后续提问,比如为什么泛型是由类型擦除来实现的或者给你展示一些会导致编译器出错的错误泛型代码。请阅读我的Java中泛型是如何工作的来了解更多信息。 - 3. 什么是泛型中的限定通配符和非限定通配符 ? - -  这是另一个非常流行的Java泛型面试题。限定通配符对类型进行了限制。有两种限定通配符,一种是它通过确保类型必须是T的子类来设定类型的上界,另一种是它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面表示了非限定通配符,因为可以用任意类型来替代。更多信息请参阅我的文章泛型中限定通配符和非限定通配符之间的区别。 - +这是另一个非常流行的Java泛型面试题。限定通配符对类型进行了限制。有两种限定通配符,一种是它通过确保类型必须是T的子类来设定类型的上界,另一种是它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面表示了非限定通配符,因为可以用任意类型来替代。更多信息请参阅我的文章泛型中限定通配符和非限定通配符之间的区别。 - 4. List和List 之间有什么区别 ? - -  这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List可以接受任何继承自T的类型的List,而List可以接受任何T的父类构成的List。例如List可以接受List或List。在本段出现的连接中可以找到更多信息。 - +这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List可以接受任何继承自T的类型的List,而List可以接受任何T的父类构成的List。例如List可以接受List或List。在本段出现的连接中可以找到更多信息。 - 5. 如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型? - -  编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样: +编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样: ``` public V put(K key, V value) { @@ -365,28 +357,21 @@ http://www.importnew.com/24029.html } ``` - 6. Java中如何使用泛型编写带有参数的类? - -  这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。 - +这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。 - 7. 编写一段泛型程序来实现LRU缓存? - -  对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。 - +对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。 - 8. 你可以把List传递给一个接受List参数的方法吗? - -  对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List应当可以用在需要List的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List可以存储任何类型的对象包括String, Integer等等,而List却只能用来存储Strings。  +对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List应当可以用在需要List的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List可以存储任何类型的对象包括String, Integer等等,而List却只能用来存储Strings。  ``` List objectList; List stringList; objectList = stringList; //compilation error incompatible types ``` - 9. Array中可以用泛型吗? - -  这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。 +这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。 - 10. 如何阻止Java中的类型未检查的警告? - -  如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如   +如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如   List rawList = new ArrayList() From 5a0c6fe4dc296607a72a4e002851b5a58147fa3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 17 Aug 2019 12:31:50 +0800 Subject: [PATCH 025/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 50f6c434..9f7c0137 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -369,9 +369,9 @@ http://www.importnew.com/24029.html ``` - 9. Array中可以用泛型吗? 这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。 - - 10. 如何阻止Java中的类型未检查的警告? 如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如   - - List rawList = new ArrayList() +``` +List rawList = new ArrayList() +``` From f40de9a47ce3052d7b5d4c7e19104c0db049dcb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 17 Aug 2019 12:34:25 +0800 Subject: [PATCH 026/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 2 ++ 1 file changed, 2 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 9f7c0137..7ecea534 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -362,11 +362,13 @@ http://www.importnew.com/24029.html 对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。 - 8. 你可以把List传递给一个接受List参数的方法吗? 对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List应当可以用在需要List的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List可以存储任何类型的对象包括String, Integer等等,而List却只能用来存储Strings。  + ``` List objectList; List stringList; objectList = stringList; //compilation error incompatible types ``` + - 9. Array中可以用泛型吗? 这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。 - 10. 如何阻止Java中的类型未检查的警告? From a92d6ba5fef728f29d9d06504517483df9450677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 17 Aug 2019 12:34:56 +0800 Subject: [PATCH 027/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 1 - 1 file changed, 1 deletion(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 7ecea534..e32b0a6c 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -362,7 +362,6 @@ http://www.importnew.com/24029.html 对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。 - 8. 你可以把List传递给一个接受List参数的方法吗? 对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List应当可以用在需要List的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List可以存储任何类型的对象包括String, Integer等等,而List却只能用来存储Strings。  - ``` List objectList; List stringList; From bd93bad3a9a3081268e129e9bd4dbaab72791523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 17 Aug 2019 12:35:35 +0800 Subject: [PATCH 028/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...7\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 5 ----- 1 file changed, 5 deletions(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index e32b0a6c..3c62bfe5 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -362,11 +362,6 @@ http://www.importnew.com/24029.html 对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。 - 8. 你可以把List传递给一个接受List参数的方法吗? 对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List应当可以用在需要List的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List可以存储任何类型的对象包括String, Integer等等,而List却只能用来存储Strings。  -``` - List objectList; - List stringList; - objectList = stringList; //compilation error incompatible types -``` - 9. Array中可以用泛型吗? 这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。 From 2a6d1658c9de1ba297eeffbdd14e882fe2e29c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 17 Aug 2019 12:37:16 +0800 Subject: [PATCH 029/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...7\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 3c62bfe5..c07a4085 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -362,10 +362,9 @@ http://www.importnew.com/24029.html 对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。 - 8. 你可以把List传递给一个接受List参数的方法吗? 对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List应当可以用在需要List的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List可以存储任何类型的对象包括String, Integer等等,而List却只能用来存储Strings。  - -- 9. Array中可以用泛型吗? +- 9.Array中可以用泛型吗? 这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。 -- 10. 如何阻止Java中的类型未检查的警告? +- 10.如何阻止Java中的类型未检查的警告? 如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如   ``` List rawList = new ArrayList() From cda64395c85f72813ebee3df23fafdeb2ee0d0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 17 Aug 2019 12:40:24 +0800 Subject: [PATCH 030/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...71\345\222\214\347\255\224\346\241\210.md" | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index c07a4085..1314b4bd 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -338,16 +338,15 @@ public void abstract printVersion(); https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html # 1.30 泛型 http://www.importnew.com/24029.html -- 1. Java中的泛型是什么 ? 使用泛型的好处是什么? +- (1)Java中的泛型是什么 ? 使用泛型的好处是什么? 这是在各种Java泛型面试中,一开场你就会被问到的问题中的一个,主要集中在初级和中级面试中。那些拥有Java1.4或更早版本的开发背景的人都知道,在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。 -- 2. Java的泛型是如何工作的 ? 什么是类型擦除 ? +- (2)Java的泛型是如何工作的 ? 什么是类型擦除 ? 这是一道更好的泛型面试题。泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。根据你对这个泛型问题的回答情况,你会得到一些后续提问,比如为什么泛型是由类型擦除来实现的或者给你展示一些会导致编译器出错的错误泛型代码。请阅读我的Java中泛型是如何工作的来了解更多信息。 - -- 3. 什么是泛型中的限定通配符和非限定通配符 ? +- (3)什么是泛型中的限定通配符和非限定通配符 ? 这是另一个非常流行的Java泛型面试题。限定通配符对类型进行了限制。有两种限定通配符,一种是它通过确保类型必须是T的子类来设定类型的上界,另一种是它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面表示了非限定通配符,因为可以用任意类型来替代。更多信息请参阅我的文章泛型中限定通配符和非限定通配符之间的区别。 -- 4. List和List 之间有什么区别 ? +- (4)List和List 之间有什么区别 ? 这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List可以接受任何继承自T的类型的List,而List可以接受任何T的父类构成的List。例如List可以接受List或List。在本段出现的连接中可以找到更多信息。 -- 5. 如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型? +- (5)如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型? 编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样: ``` public V put(K key, V value) { @@ -356,15 +355,15 @@ http://www.importnew.com/24029.html } ``` -- 6. Java中如何使用泛型编写带有参数的类? +- (6)Java中如何使用泛型编写带有参数的类? 这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。 -- 7. 编写一段泛型程序来实现LRU缓存? +- (7)编写一段泛型程序来实现LRU缓存? 对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。 -- 8. 你可以把List传递给一个接受List参数的方法吗? +- (8)你可以把List传递给一个接受List参数的方法吗? 对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List应当可以用在需要List的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List可以存储任何类型的对象包括String, Integer等等,而List却只能用来存储Strings。  -- 9.Array中可以用泛型吗? +- (9)Array中可以用泛型吗? 这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。 -- 10.如何阻止Java中的类型未检查的警告? +- (1)) 如何阻止Java中的类型未检查的警告? 如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如   ``` List rawList = new ArrayList() From 8e9a9377170fe839cfff9e849818b7dd4c1f4e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 17 Aug 2019 13:25:38 +0800 Subject: [PATCH 031/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...71\345\222\214\347\255\224\346\241\210.md" | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 1314b4bd..52f2d7ce 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -369,3 +369,75 @@ http://www.importnew.com/24029.html List rawList = new ArrayList() ``` +# 1.31 泛型 ?与T的区别 +https://blog.csdn.net/woshizisezise/article/details/79374460 +public static void show1(List list){ + for (Object object : list) { + System.out.println(object.toString()); + } +} + +public static void show2(List list) { + for (Object object : list) { + System.out.println(object); + } +} +public static void test(){ + List list1 = new ArrayList<>(); + list1.add(new Student("zhangsan",18,0)); + list1.add(new Student("lisi",28,0)); + list1.add(new Student("wangwu",24,1)); + //这里如果add(new Teacher(...));就会报错,因为我们已经给List指定了数据类型为Student + show1(list1); + + System.out.println("************分割线**************"); + + //这里我们并没有给List指定具体的数据类型,可以存放多种类型数据 + List list2 = new ArrayList<>(); + list2.add(new Student("zhaoliu",22,1)); + list2.add(new Teacher("sunba",30,0)); + show2(list2); +} +从show2方法可以看出和show1的区别了,list2存放了Student和Teacher两种类型,同样可以输出数据,所以这就是T和?的区别啦 +# 1.32 字节流和字符流的区别 + +https://www.cnblogs.com/huangliting/p/5746950.html +https://www.cnblogs.com/huangliting/p/5746950.html +## 字节流: +![](https://images2015.cnblogs.com/blog/1004782/201608/1004782-20160807193514106-1603602397.png) +![](https://images2015.cnblogs.com/blog/1004782/201608/1004782-20160807193524637-1218858412.png) +- (A)FileOutputStream(File name) 创建一个文件输出流,向指定的 File 对象输出数据。 +- (B)FileOutputStream(FileDescriptor) 创建一个文件输出流,向指定的文件描述器输出数据。 +- (C)FileOutputStream(String name) 创建一个文件输出流,向指定名称的文件输出数据。 +- (D)FileOutputStream(String, boolean) 用指定系统的文件名,创建一个输出文件。 + +![](https://images2015.cnblogs.com/blog/1004782/201608/1004782-20160807193558668-1200087629.png) +![](https://images2015.cnblogs.com/blog/1004782/201608/1004782-20160807193607762-955819376.png) +InputStreamReader 和 OutputStreamReader : + +把一个以字节为导向的 stream 转换成一个以字符为导向的 stream 。 +InputStreamReader 类是从字节流到字符流的桥梁:它读入字节,并根据指定的编码方式,将之转换为字符流。 +使用的编码方式可能由名称指定,或平台可接受的缺省编码方式。 +InputStreamReader 的 read() 方法之一的每次调用,可能促使从基本字节输入流中读取一个或多个字节。 +为了达到更高效率,考虑用 BufferedReader 封装 InputStreamReader , +BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + +## 相同点: + +InputStream,OutputStream,Reader,writer都是抽象类。所以不能直接new + +## 字节流与字符流的区别 + +字节流和字符流使用是非常相似的,那么除了操作代码的不同之外,还有哪些不同呢? +区别: +- 1、字节流在操作的时候本身是不会用到缓冲区(内存)的,是与文件本身直接操作的,而字符流在操作的时候是使用到缓冲区的 +- 2、字节流在操作文件时,即使不关闭资源(close方法),文件也能输出,但是如果字符流不使用close方法的话,则不会输出任何内容,说明字符流用的是缓冲区,并且可以使用flush方法强制进行刷新缓冲区,这时才能在不close的情况下输出内容 +- 3、Reader类的read()方法返回类型为int :作为整数读取的字符(占两个字节共16位),范围在 0 到 65535 之间 (0x00-0xffff),如果已到达流的末尾,则返回 -1 +inputStream的read()虽然也返回int,但由于此类是面向字节流的,一个字节占8个位,所以返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。因此对于不能用0-255来表示的值就得用字符流来读取!比如说汉字. +## 4、字节流与字符流主要的区别是他们的的处理方式 +字节流:处理字节和字节数组或二进制对象; +字符流:处理字符、字符数组或字符串。 +## 那开发中究竟用字节流好还是用字符流好呢? +一、字符(Reader和 Writer):中文,字符是只有在内存中才会形成的,操作字符、字符数组或字符串, +二、字节(InputStream 和OutputStream):音频文件、图片、歌曲,所有的硬盘上保存文件或进行传输的时候,操作字节和字节数组或二进制对象, +*如果要java程序实现一个拷贝功能,应该选用字节流进行操作(可能拷贝的是图片),并且采用边读边写的方式(节省内存)。 From 2a771ce4f7492c55cd1ca66d3bc5bd61487cedb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 17 Aug 2019 13:26:32 +0800 Subject: [PATCH 032/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" | 1 - 1 file changed, 1 deletion(-) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 52f2d7ce..ea89d75c 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -414,7 +414,6 @@ https://www.cnblogs.com/huangliting/p/5746950.html ![](https://images2015.cnblogs.com/blog/1004782/201608/1004782-20160807193558668-1200087629.png) ![](https://images2015.cnblogs.com/blog/1004782/201608/1004782-20160807193607762-955819376.png) InputStreamReader 和 OutputStreamReader : - 把一个以字节为导向的 stream 转换成一个以字符为导向的 stream 。 InputStreamReader 类是从字节流到字符流的桥梁:它读入字节,并根据指定的编码方式,将之转换为字符流。 使用的编码方式可能由名称指定,或平台可接受的缺省编码方式。 From a1111d06a938220be6b256b2342bcf8fd1292160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 17 Aug 2019 13:38:57 +0800 Subject: [PATCH 033/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...71\345\222\214\347\255\224\346\241\210.md" | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index ea89d75c..2d4c4bac 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -263,6 +263,11 @@ finally 一定会被执行,如果 finally 里有 return 语句,则覆盖 try 比较爱考的是 finally 里没有 return 语句,这时虽然 finally 里对 return 的值进行了修改,但 return 的值并不改变这种情况 ## 三.finally代码块和finalize()方法有什么区别? 无论是否抛出异常,finally代码块都会执行,它主要是用来释放应用占用的资源。finalize()方法是Object类的一个protected方法,它是在对象被垃圾回收之前由Java虚拟机来调用的。 +## 四. finally到底是在return之前执行还是return之后执行? +https://mp.weixin.qq.com/s?__biz=MzI5MzYzMDAwNw==&mid=2247485244&idx=1&sn=162035183aa027b887fc642fddc4ad69&scene=19&token=557705008&lang=zh_CN#wechat_redirect +## 五. final关键字详解 +https://mp.weixin.qq.com/s?__biz=MzI5MzYzMDAwNw==&mid=2247485027&idx=1&sn=f565be7c5ce33e9737d94ce9a58bd0ea&scene=19&token=557705008&lang=zh_CN#wechat_redirect + # 1.23 public private default protected ![](http://ww1.sinaimg.cn/large/007s8HJUly1g5yfwftsjrj30ha06wjsq.jpg) 不写时默认为default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。 @@ -371,6 +376,7 @@ List rawList = new ArrayList() # 1.31 泛型 ?与T的区别 https://blog.csdn.net/woshizisezise/article/details/79374460 +``` public static void show1(List list){ for (Object object : list) { System.out.println(object.toString()); @@ -398,6 +404,7 @@ public static void test(){ list2.add(new Teacher("sunba",30,0)); show2(list2); } +``` 从show2方法可以看出和show1的区别了,list2存放了Student和Teacher两种类型,同样可以输出数据,所以这就是T和?的区别啦 # 1.32 字节流和字符流的区别 @@ -440,3 +447,20 @@ inputStream的read()虽然也返回int,但由于此类是面向字节流的, 一、字符(Reader和 Writer):中文,字符是只有在内存中才会形成的,操作字符、字符数组或字符串, 二、字节(InputStream 和OutputStream):音频文件、图片、歌曲,所有的硬盘上保存文件或进行传输的时候,操作字节和字节数组或二进制对象, *如果要java程序实现一个拷贝功能,应该选用字节流进行操作(可能拷贝的是图片),并且采用边读边写的方式(节省内存)。 +# 1.33 父子类的加载顺序? +>面试和笔试经常考 + +详情可以看: +https://mp.weixin.qq.com/s?__biz=MzI5MzYzMDAwNw==&mid=2247485357&idx=1&sn=4cfda217b421eb5144d7b873894b5206&scene=19&token=557705008&lang=zh_CN#wechat_redirect + +类的加载顺序。 +- (1) 父类静态代码块(包括静态初始化块,静态属性,但不包括静态方法) +- (2) 子类静态代码块(包括静态初始化块,静态属性,但不包括静态方法 ) +- (3) 父类非静态代码块( 包括非静态初始化块,非静态属性 ) +- (4) 父类构造函数 +- (5) 子类非静态代码块 ( 包括非静态初始化块,非静态属性 ) +- (6) 子类构造函数 + +# 1.34 什么是字符集和编码? + +https://mp.weixin.qq.com/s?__biz=MzI5MzYzMDAwNw==&mid=2247484848&idx=1&sn=ad7f134c40574ec1214df28b078c88e1&scene=19&token=557705008&lang=zh_CN#wechat_redirect From 67a49a833980639e48f68cffd64935188f27d7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 17 Aug 2019 22:49:00 +0800 Subject: [PATCH 034/323] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E5=92=8C=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2\271\345\222\214\347\255\224\346\241\210.md" | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" index 2d4c4bac..e0c6c14b 100644 --- "a/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" +++ "b/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\202\271\345\222\214\347\255\224\346\241\210.md" @@ -464,3 +464,19 @@ https://mp.weixin.qq.com/s?__biz=MzI5MzYzMDAwNw==&mid=2247485357&idx=1&sn=4cfda2 # 1.34 什么是字符集和编码? https://mp.weixin.qq.com/s?__biz=MzI5MzYzMDAwNw==&mid=2247484848&idx=1&sn=ad7f134c40574ec1214df28b078c88e1&scene=19&token=557705008&lang=zh_CN#wechat_redirect + +# 1.35 Math.round(11.5)等於多少? Math.round(-11.5)等於多少? +Math 类中提供了三个与取整有关的方法:ceil、floor、round,这些方法的作用与它们的英 +文名称的含义相对应,例如,ceil 的英文意义是天花板,该方法就表示向上取整, +Math.ceil(11.3)的结果为12,Math.ceil(-11.3)的结果是-11;floor 的英文意义是地板,该方法 +就表示向下取整,Math.ceil(11.6)的结果为11,Math.ceil(-11.6)的结果是-12;最难掌握的是 +round 方法,它表示“四舍五入”,算法为 Math.floor(x+0.5),即将原来的数字加上0.5后再向 +下取整,所以,Math.round(11.5)的结果为12,Math.round(-11.5)的结果为-11。 +# 1.36 char 型变量中能不能存贮一个中文汉字?为什么? +>作业帮面试题 + +char 型变量是用来存储 Unicode 编码的字符的,unicode 编码字符集中包含了汉字,所以, +char 型变量中当然可以存储汉字啦。不过,如果某个特殊的汉字没有被包含在 unicode 编 +码字符集中,那么,这个 char 型变量中就不能存储这个特殊汉字。补充说明:unicode 编 +码占用两个字节,所以,char 类型的变量也是占用两个字节。 +# 1.37 From 4204d245826785b8247d5cc2210695a38c0485fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Mon, 26 Aug 2019 23:27:26 +0800 Subject: [PATCH 035/323] Create pic --- pic | 1 + 1 file changed, 1 insertion(+) create mode 100644 pic diff --git a/pic b/pic new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/pic @@ -0,0 +1 @@ +1 From 0ffc578dbca8fefd586c4bbb6eca70fe4f73e77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Mon, 26 Aug 2019 23:28:20 +0800 Subject: [PATCH 036/323] Delete pic --- pic | 1 - 1 file changed, 1 deletion(-) delete mode 100644 pic diff --git a/pic b/pic deleted file mode 100644 index d00491fd..00000000 --- a/pic +++ /dev/null @@ -1 +0,0 @@ -1 From cb3c9ba84cc6ecfad926f2140ba961a59ee2f1bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Mon, 26 Aug 2019 23:32:13 +0800 Subject: [PATCH 037/323] Create 1.md --- .../1.md" | 1 + 1 file changed, 1 insertion(+) create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/1.md" diff --git "a/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/1.md" "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/1.md" new file mode 100644 index 00000000..8b137891 --- /dev/null +++ "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/1.md" @@ -0,0 +1 @@ + From 78c4dbebdc6074444ed1c027ce889a935283f427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Mon, 26 Aug 2019 23:35:47 +0800 Subject: [PATCH 038/323] Add files via upload --- .../notes/Docker.md" | 100 + .../notes/Git.md" | 171 + .../notes/HTTP.md" | 890 +++++ .../notes/Java IO.md" | 631 ++++ .../notes/Java \345\237\272\347\241\200.md" | 1446 ++++++++ .../notes/Java \345\256\271\345\231\250.md" | 1163 +++++++ .../notes/Java \345\271\266\345\217\221.md" | 1647 +++++++++ ...a \350\231\232\346\213\237\346\234\272.md" | 769 ++++ ...14\345\210\206\346\237\245\346\211\276.md" | 302 ++ ...- \344\275\215\350\277\220\347\256\227.md" | 429 +++ ...350\247\243 - \345\210\206\346\262\273.md" | 117 + ...50\346\200\201\350\247\204\345\210\222.md" | 1263 +++++++ ...- \345\217\214\346\214\207\351\222\210.md" | 251 ++ ...- \345\223\210\345\270\214\350\241\250.md" | 134 + ...351\242\230\350\247\243 - \345\233\276.md" | 268 ++ ...- \345\255\227\347\254\246\344\270\262.md" | 236 ++ ...350\247\243 - \346\216\222\345\272\217.md" | 241 ++ ...350\247\243 - \346\220\234\347\264\242.md" | 1280 +++++++ ...350\247\243 - \346\225\260\345\255\246.md" | 518 +++ ...04\344\270\216\347\237\251\351\230\265.md" | 444 +++ ...10\345\222\214\351\230\237\345\210\227.md" | 231 ++ ...351\242\230\350\247\243 - \346\240\221.md" | 1131 ++++++ ...350\247\243 - \347\233\256\345\275\225.md" | 46 + ...50\247\243 - \347\233\256\345\275\2251.md" | 46 + ...52\345\277\203\346\200\235\346\203\263.md" | 379 ++ ...350\247\243 - \351\223\276\350\241\250.md" | 354 ++ .../Leetcode \351\242\230\350\247\243.md" | 16 + ...code-Database \351\242\230\350\247\243.md" | 1016 ++++++ .../notes/Linux.md" | 1256 +++++++ .../notes/MySQL.md" | 434 +++ .../notes/Redis.md" | 619 ++++ .../notes/SQL.md" | 782 +++++ .../notes/Socket.md" | 346 ++ ...01\345\217\257\350\257\273\346\200\247.md" | 344 ++ ...16\346\240\274\350\247\204\350\214\203.md" | 18 + .../\345\210\206\345\270\203\345\274\217.md" | 353 ++ ...Offer \351\242\230\350\247\243 - 10~19.md" | 703 ++++ ...Offer \351\242\230\350\247\243 - 20~29.md" | 401 +++ ...Offer \351\242\230\350\247\243 - 30~39.md" | 481 +++ ...7 Offer \351\242\230\350\247\243 - 3~9.md" | 372 ++ ...Offer \351\242\230\350\247\243 - 40~49.md" | 429 +++ ...Offer \351\242\230\350\247\243 - 50~59.md" | 490 +++ ...Offer \351\242\230\350\247\243 - 60~68.md" | 333 ++ ...350\247\243 - \347\233\256\345\275\225.md" | 26 + ...50\247\243 - \347\233\256\345\275\2251.md" | 26 + ...214\207 offer \351\242\230\350\247\243.md" | 16 + ...73\345\207\273\346\212\200\346\234\257.md" | 204 ++ ...73\347\273\237\345\216\237\347\220\206.md" | 585 ++++ ...04\345\273\272\345\267\245\345\205\267.md" | 152 + ...31\350\241\250\350\276\276\345\274\217.md" | 399 +++ ...10\346\201\257\351\230\237\345\210\227.md" | 90 + ...346\263\225 - \345\205\266\345\256\203.md" | 145 + ...- \345\271\266\346\237\245\351\233\206.md" | 203 ++ ...346\263\225 - \346\216\222\345\272\217.md" | 593 ++++ ...10\345\222\214\351\230\237\345\210\227.md" | 329 ++ ...346\263\225 - \347\233\256\345\275\225.md" | 25 + ...46\263\225 - \347\233\256\345\275\2251.md" | 25 + ...- \347\254\246\345\217\267\350\241\250.md" | 951 +++++ ...27\346\263\225\345\210\206\346\236\220.md" | 247 ++ .../notes/\347\256\227\346\263\225.md" | 16 + ...76\350\256\241\345\237\272\347\241\200.md" | 120 + .../notes/\347\274\223\345\255\230.md" | 319 ++ ...05\345\255\230\347\256\241\347\220\206.md" | 151 + ...347\273\237 - \346\246\202\350\277\260.md" | 133 + ...347\273\237 - \346\255\273\351\224\201.md" | 153 + ...347\273\237 - \347\233\256\345\275\225.md" | 33 + ...47\273\237 - \347\233\256\345\275\2251.md" | 33 + ...76\345\244\207\347\256\241\347\220\206.md" | 70 + ...33\347\250\213\347\256\241\347\220\206.md" | 599 ++++ ...347\273\237 - \351\223\276\346\216\245.md" | 77 + ...15\344\275\234\347\263\273\347\273\237.md" | 16 + ...- \344\274\240\350\276\223\345\261\202.md" | 176 + ...- \345\272\224\347\224\250\345\261\202.md" | 177 + ...347\273\234 - \346\246\202\350\277\260.md" | 143 + ...- \347\211\251\347\220\206\345\261\202.md" | 31 + ...347\273\234 - \347\233\256\345\275\225.md" | 39 + ...47\273\234 - \347\233\256\345\275\2251.md" | 40 + ...- \347\275\221\347\273\234\345\261\202.md" | 253 ++ ...- \351\223\276\350\267\257\345\261\202.md" | 207 ++ ...27\346\234\272\347\275\221\347\273\234.md" | 16 + ...76\350\256\241\346\250\241\345\274\217.md" | 3077 +++++++++++++++++ .../notes/\351\233\206\347\276\244.md" | 212 ++ ...71\350\261\241\346\200\235\346\203\263.md" | 381 ++ 83 files changed, 33368 insertions(+) create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Docker.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Git.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/HTTP.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java IO.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java \345\237\272\347\241\200.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java \345\256\271\345\231\250.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java \345\271\266\345\217\221.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java \350\231\232\346\213\237\346\234\272.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \344\272\214\345\210\206\346\237\245\346\211\276.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \344\275\215\350\277\220\347\256\227.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \345\210\206\346\262\273.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \345\212\250\346\200\201\350\247\204\345\210\222.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \345\217\214\346\214\207\351\222\210.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \345\223\210\345\270\214\350\241\250.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \345\233\276.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \345\255\227\347\254\246\344\270\262.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \346\216\222\345\272\217.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \346\220\234\347\264\242.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \346\225\260\345\255\246.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \346\225\260\347\273\204\344\270\216\347\237\251\351\230\265.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \346\240\210\345\222\214\351\230\237\345\210\227.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \346\240\221.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \347\233\256\345\275\225.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \347\233\256\345\275\2251.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \350\264\252\345\277\203\346\200\235\346\203\263.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243 - \351\223\276\350\241\250.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode \351\242\230\350\247\243.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Leetcode-Database \351\242\230\350\247\243.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Linux.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/MySQL.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Redis.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/SQL.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Socket.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\344\273\243\347\240\201\345\217\257\350\257\273\346\200\247.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\344\273\243\347\240\201\351\243\216\346\240\274\350\247\204\350\214\203.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\345\210\206\345\270\203\345\274\217.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\345\211\221\346\214\207 Offer \351\242\230\350\247\243 - 10~19.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\345\211\221\346\214\207 Offer \351\242\230\350\247\243 - 20~29.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\345\211\221\346\214\207 Offer \351\242\230\350\247\243 - 30~39.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\345\211\221\346\214\207 Offer \351\242\230\350\247\243 - 3~9.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\345\211\221\346\214\207 Offer \351\242\230\350\247\243 - 40~49.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\345\211\221\346\214\207 Offer \351\242\230\350\247\243 - 50~59.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\345\211\221\346\214\207 Offer \351\242\230\350\247\243 - 60~68.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\345\211\221\346\214\207 Offer \351\242\230\350\247\243 - \347\233\256\345\275\225.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\345\211\221\346\214\207 Offer \351\242\230\350\247\243 - \347\233\256\345\275\2251.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\345\211\221\346\214\207 offer \351\242\230\350\247\243.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\346\224\273\345\207\273\346\212\200\346\234\257.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237\345\216\237\347\220\206.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\346\236\204\345\273\272\345\267\245\345\205\267.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\346\266\210\346\201\257\351\230\237\345\210\227.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\347\256\227\346\263\225 - \345\205\266\345\256\203.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\347\256\227\346\263\225 - \345\271\266\346\237\245\351\233\206.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\347\256\227\346\263\225 - \346\216\222\345\272\217.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\347\256\227\346\263\225 - \346\240\210\345\222\214\351\230\237\345\210\227.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\347\256\227\346\263\225 - \347\233\256\345\275\225.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\347\256\227\346\263\225 - \347\233\256\345\275\2251.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\347\256\227\346\263\225 - \347\254\246\345\217\267\350\241\250.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\347\256\227\346\263\225 - \347\256\227\346\263\225\345\210\206\346\236\220.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\347\256\227\346\263\225.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\347\263\273\347\273\237\350\256\276\350\256\241\345\237\272\347\241\200.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\347\274\223\345\255\230.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237 - \345\206\205\345\255\230\347\256\241\347\220\206.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237 - \346\246\202\350\277\260.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237 - \346\255\273\351\224\201.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237 - \347\233\256\345\275\225.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237 - \347\233\256\345\275\2251.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237 - \350\256\276\345\244\207\347\256\241\347\220\206.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237 - \350\277\233\347\250\213\347\256\241\347\220\206.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237 - \351\223\276\346\216\245.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \344\274\240\350\276\223\345\261\202.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \345\272\224\347\224\250\345\261\202.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \346\246\202\350\277\260.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \347\211\251\347\220\206\345\261\202.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \347\233\256\345\275\225.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \347\233\256\345\275\2251.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \347\275\221\347\273\234\345\261\202.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \351\223\276\350\267\257\345\261\202.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\350\256\276\350\256\241\346\250\241\345\274\217.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\351\233\206\347\276\244.md" create mode 100644 "\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/\351\235\242\345\220\221\345\257\271\350\261\241\346\200\235\346\203\263.md" diff --git "a/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Docker.md" "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Docker.md" new file mode 100644 index 00000000..bc610eb7 --- /dev/null +++ "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Docker.md" @@ -0,0 +1,100 @@ + +* [一、解决的问题](#一解决的问题) +* [二、与虚拟机的比较](#二与虚拟机的比较) +* [三、优势](#三优势) +* [四、使用场景](#四使用场景) +* [五、镜像与容器](#五镜像与容器) +* [参考资料](#参考资料) + + + +# 一、解决的问题 + +由于不同的机器有不同的操作系统,以及不同的库和组件,在将一个应用部署到多台机器上需要进行大量的环境配置操作。 + +Docker 主要解决环境配置问题,它是一种虚拟化技术,对进程进行隔离,被隔离的进程独立于宿主操作系统和其它隔离的进程。使用 Docker 可以不修改应用程序代码,不需要开发人员学习特定环境下的技术,就能够将现有的应用程序部署在其它机器上。 + +

+ +# 二、与虚拟机的比较 + +虚拟机也是一种虚拟化技术,它与 Docker 最大的区别在于它是通过模拟硬件,并在硬件上安装操作系统来实现。 + +

+ +## 启动速度 + +启动虚拟机需要先启动虚拟机的操作系统,再启动应用,这个过程非常慢; + +而启动 Docker 相当于启动宿主操作系统上的一个进程。 + +## 占用资源 + +虚拟机是一个完整的操作系统,需要占用大量的磁盘、内存和 CPU 资源,一台机器只能开启几十个的虚拟机。 + +而 Docker 只是一个进程,只需要将应用以及相关的组件打包,在运行时占用很少的资源,一台机器可以开启成千上万个 Docker。 + +# 三、优势 + +除了启动速度快以及占用资源少之外,Docker 具有以下优势: + +## 更容易迁移 + +提供一致性的运行环境。已经打包好的应用可以在不同的机器上进行迁移,而不用担心环境变化导致无法运行。 + +## 更容易维护 + +使用分层技术和镜像,使得应用可以更容易复用重复的部分。复用程度越高,维护工作也越容易。 + +## 更容易扩展 + +可以使用基础镜像进一步扩展得到新的镜像,并且官方和开源社区提供了大量的镜像,通过扩展这些镜像可以非常容易得到我们想要的镜像。 + +# 四、使用场景 + +## 持续集成 + +持续集成指的是频繁地将代码集成到主干上,这样能够更快地发现错误。 + +Docker 具有轻量级以及隔离性的特点,在将代码集成到一个 Docker 中不会对其它 Docker 产生影响。 + +## 提供可伸缩的云服务 + +根据应用的负载情况,可以很容易地增加或者减少 Docker。 + +## 搭建微服务架构 + +Docker 轻量级的特点使得它很适合用于部署、维护、组合微服务。 + +# 五、镜像与容器 + +镜像是一种静态的结构,可以看成面向对象里面的类,而容器是镜像的一个实例。 + +镜像包含着容器运行时所需要的代码以及其它组件,它是一种分层结构,每一层都是只读的(read-only layers)。构建镜像时,会一层一层构建,前一层是后一层的基础。镜像的这种分层存储结构很适合镜像的复用以及定制。 + +构建容器时,通过在镜像的基础上添加一个可写层(writable layer),用来保存着容器运行过程中的修改。 + +

+ +# 参考资料 + +- [DOCKER 101: INTRODUCTION TO DOCKER WEBINAR RECAP](https://blog.docker.com/2017/08/docker-101-introduction-docker-webinar-recap/) +- [Docker 入门教程](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html) +- [Docker container vs Virtual machine](http://www.bogotobogo.com/DevOps/Docker/Docker_Container_vs_Virtual_Machine.php) +- [How to Create Docker Container using Dockerfile](https://linoxide.com/linux-how-to/dockerfile-create-docker-container/) +- [理解 Docker(2):Docker 镜像](http://www.cnblogs.com/sammyliu/p/5877964.html) +- [为什么要使用 Docker?](https://yeasy.gitbooks.io/docker_practice/introduction/why.html) +- [What is Docker](https://www.docker.com/what-docker) +- [持续集成是什么?](http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html) + + + + + +# 微信公众号 + + +更多精彩内容将发布在微信公众号 CyC2018 上,你也可以在公众号后台和我交流学习和求职相关的问题。另外,公众号提供了该项目的 PDF 等离线阅读版本,后台回复 "下载" 即可领取。公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲" 即可领取。我基本是按照这个大纲来进行复习的,对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。 + + +
diff --git "a/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Git.md" "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Git.md" new file mode 100644 index 00000000..01399a41 --- /dev/null +++ "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Git.md" @@ -0,0 +1,171 @@ + +* [集中式与分布式](#集中式与分布式) +* [中心服务器](#中心服务器) +* [工作流](#工作流) +* [分支实现](#分支实现) +* [冲突](#冲突) +* [Fast forward](#fast-forward) +* [分支管理策略](#分支管理策略) +* [储藏(Stashing)](#储藏stashing) +* [SSH 传输设置](#ssh-传输设置) +* [.gitignore 文件](#gitignore-文件) +* [Git 命令一览](#git-命令一览) +* [参考资料](#参考资料) + + + +# 集中式与分布式 + +Git 属于分布式版本控制系统,而 SVN 属于集中式。 + +

+ +集中式版本控制只有中心服务器拥有一份代码,而分布式版本控制每个人的电脑上就有一份完整的代码。 + +集中式版本控制有安全性问题,当中心服务器挂了所有人都没办法工作了。 + +集中式版本控制需要连网才能工作,如果网速过慢,那么提交一个文件会慢的无法让人忍受。而分布式版本控制不需要连网就能工作。 + +分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。 + +# 中心服务器 + +中心服务器用来交换每个用户的修改,没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。 + +Github 就是一个中心服务器。 + +# 工作流 + +新建一个仓库之后,当前目录就成为了工作区,工作区下有一个隐藏目录 .git,它属于 Git 的版本库。 + +Git 的版本库有一个称为 Stage 的暂存区以及最后的 History 版本库,History 存储所有分支信息,使用一个 HEAD 指针指向当前分支。 + +

+ +- git add files 把文件的修改添加到暂存区 +- git commit 把暂存区的修改提交到当前分支,提交之后暂存区就被清空了 +- git reset -- files 使用当前分支上的修改覆盖暂存区,用来撤销最后一次 git add files +- git checkout -- files 使用暂存区的修改覆盖工作目录,用来撤销本地修改 + +

+ +可以跳过暂存区域直接从分支中取出修改,或者直接提交修改到分支中。 + +- git commit -a 直接把所有文件的修改添加到暂存区然后执行提交 +- git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作 + +

+ +# 分支实现 + +使用指针将每个提交连接成一条时间线,HEAD 指针指向当前分支指针。 + +

+ +新建分支是新建一个指针指向时间线的最后一个节点,并让 HEAD 指针指向新分支,表示新分支成为当前分支。 + +

+ +每次提交只会让当前分支指针向前移动,而其它分支指针不会移动。 + +

+ +合并分支也只需要改变指针即可。 + +

+ +# 冲突 + +当两个分支都对同一个文件的同一行进行了修改,在分支合并时就会产生冲突。 + +

+ +Git 会使用 <<<<<<< ,======= ,>>>>>>> 标记出不同分支的内容,只需要把不同分支中冲突部分修改成一样就能解决冲突。 + +``` +<<<<<<< HEAD +Creating a new branch is quick & simple. +======= +Creating a new branch is quick AND simple. +>>>>>>> feature1 +``` + +# Fast forward + +"快进式合并"(fast-farward merge),会直接将 master 分支指向合并的分支,这种模式下进行分支合并会丢失分支信息,也就不能在分支历史上看出分支信息。 + +可以在合并时加上 --no-ff 参数来禁用 Fast forward 模式,并且加上 -m 参数让合并时产生一个新的 commit。 + +``` +$ git merge --no-ff -m "merge with no-ff" dev +``` + +

+ +# 分支管理策略 + +master 分支应该是非常稳定的,只用来发布新版本; + +日常开发在开发分支 dev 上进行。 + +

+ +# 储藏(Stashing) + +在一个分支上操作之后,如果还没有将修改提交到分支上,此时进行切换分支,那么另一个分支上也能看到新的修改。这是因为所有分支都共用一个工作区的缘故。 + +可以使用 git stash 将当前分支的修改储藏起来,此时当前工作区的所有修改都会被存到栈中,也就是说当前工作区是干净的,没有任何未提交的修改。此时就可以安全的切换到其它分支上了。 + +``` +$ git stash +Saved working directory and index state \ "WIP on master: 049d078 added the index file" +HEAD is now at 049d078 added the index file (To restore them type "git stash apply") +``` + +该功能可以用于 bug 分支的实现。如果当前正在 dev 分支上进行开发,但是此时 master 上有个 bug 需要修复,但是 dev 分支上的开发还未完成,不想立即提交。在新建 bug 分支并切换到 bug 分支之前就需要使用 git stash 将 dev 分支的未提交修改储藏起来。 + +# SSH 传输设置 + +Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密。 + +如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key: + +``` +$ ssh-keygen -t rsa -C "youremail@example.com" +``` + +然后把公钥 id_rsa.pub 的内容复制到 Github "Account settings" 的 SSH Keys 中。 + +# .gitignore 文件 + +忽略以下文件: + +- 操作系统自动生成的文件,比如缩略图; +- 编译生成的中间文件,比如 Java 编译产生的 .class 文件; +- 自己的敏感信息,比如存放口令的配置文件。 + +不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。 + +# Git 命令一览 + +

+ +比较详细的地址:http://www.cheat-sheets.org/saved-copy/git-cheat-sheet.pdf + +# 参考资料 + +- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html) +- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html) +- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000) +- [Learn Git Branching](https://learngitbranching.js.org/) + + + + +# 微信公众号 + + +更多精彩内容将发布在微信公众号 CyC2018 上,你也可以在公众号后台和我交流学习和求职相关的问题。另外,公众号提供了该项目的 PDF 等离线阅读版本,后台回复 "下载" 即可领取。公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲" 即可领取。我基本是按照这个大纲来进行复习的,对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。 + + +
diff --git "a/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/HTTP.md" "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/HTTP.md" new file mode 100644 index 00000000..8805981d --- /dev/null +++ "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/HTTP.md" @@ -0,0 +1,890 @@ + +* [一 、基础概念](#一-基础概念) + * [URI](#uri) + * [请求和响应报文](#请求和响应报文) +* [二、HTTP 方法](#二http-方法) + * [GET](#get) + * [HEAD](#head) + * [POST](#post) + * [PUT](#put) + * [PATCH](#patch) + * [DELETE](#delete) + * [OPTIONS](#options) + * [CONNECT](#connect) + * [TRACE](#trace) +* [三、HTTP 状态码](#三http-状态码) + * [1XX 信息](#1xx-信息) + * [2XX 成功](#2xx-成功) + * [3XX 重定向](#3xx-重定向) + * [4XX 客户端错误](#4xx-客户端错误) + * [5XX 服务器错误](#5xx-服务器错误) +* [四、HTTP 首部](#四http-首部) + * [通用首部字段](#通用首部字段) + * [请求首部字段](#请求首部字段) + * [响应首部字段](#响应首部字段) + * [实体首部字段](#实体首部字段) +* [五、具体应用](#五具体应用) + * [连接管理](#连接管理) + * [Cookie](#cookie) + * [缓存](#缓存) + * [内容协商](#内容协商) + * [内容编码](#内容编码) + * [范围请求](#范围请求) + * [分块传输编码](#分块传输编码) + * [多部分对象集合](#多部分对象集合) + * [虚拟主机](#虚拟主机) + * [通信数据转发](#通信数据转发) +* [六、HTTPS](#六https) + * [加密](#加密) + * [认证](#认证) + * [完整性保护](#完整性保护) + * [HTTPS 的缺点](#https-的缺点) +* [七、HTTP/2.0](#七http20) + * [HTTP/1.x 缺陷](#http1x-缺陷) + * [二进制分帧层](#二进制分帧层) + * [服务端推送](#服务端推送) + * [首部压缩](#首部压缩) +* [八、HTTP/1.1 新特性](#八http11-新特性) +* [九、GET 和 POST 比较](#九get-和-post-比较) + * [作用](#作用) + * [参数](#参数) + * [安全](#安全) + * [幂等性](#幂等性) + * [可缓存](#可缓存) + * [XMLHttpRequest](#xmlhttprequest) +* [参考资料](#参考资料) + + + +# 一 、基础概念 + +## URI + +URI 包含 URL 和 URN。 + +

+ +## 请求和响应报文 + +### 1. 请求报文 + +

+ +### 2. 响应报文 + +

+ +# 二、HTTP 方法 + +客户端发送的 **请求报文** 第一行为请求行,包含了方法字段。 + +## GET + +> 获取资源 + +当前网络请求中,绝大部分使用的是 GET 方法。 + +## HEAD + +> 获取报文首部 + +和 GET 方法类似,但是不返回报文实体主体部分。 + +主要用于确认 URL 的有效性以及资源更新的日期时间等。 + +## POST + +> 传输实体主体 + +POST 主要用来传输数据,而 GET 主要用来获取资源。 + +更多 POST 与 GET 的比较请见第九章。 + +## PUT + +> 上传文件 + +由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。 + +```html +PUT /new.html HTTP/1.1 +Host: example.com +Content-type: text/html +Content-length: 16 + +

New File

+``` + +## PATCH + +> 对资源进行部分修改 + +PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。 + +```html +PATCH /file.txt HTTP/1.1 +Host: www.example.com +Content-Type: application/example +If-Match: "e0023aa4e" +Content-Length: 100 + +[description of changes] +``` + +## DELETE + +> 删除文件 + +与 PUT 功能相反,并且同样不带验证机制。 + +```html +DELETE /file.html HTTP/1.1 +``` + +## OPTIONS + +> 查询支持的方法 + +查询指定的 URL 能够支持的方法。 + +会返回 `Allow: GET, POST, HEAD, OPTIONS` 这样的内容。 + +## CONNECT + +> 要求在与代理服务器通信时建立隧道 + +使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。 + +```html +CONNECT www.example.com:443 HTTP/1.1 +``` + +

+ +## TRACE + +> 追踪路径 + +服务器会将通信路径返回给客户端。 + +发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。 + +通常不会使用 TRACE,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪)。 + +# 三、HTTP 状态码 + +服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。 + +| 状态码 | 类别 | 含义 | +| :---: | :---: | :---: | +| 1XX | Informational(信息性状态码) | 接收的请求正在处理 | +| 2XX | Success(成功状态码) | 请求正常处理完毕 | +| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 | +| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 | +| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 | + +## 1XX 信息 + +- **100 Continue** :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。 + +## 2XX 成功 + +- **200 OK** + +- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 + +- **206 Partial Content** :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。 + +## 3XX 重定向 + +- **301 Moved Permanently** :永久性重定向 + +- **302 Found** :临时性重定向 + +- **303 See Other** :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。 + +- 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。 + +- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。 + +- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。 + +## 4XX 客户端错误 + +- **400 Bad Request** :请求报文中存在语法错误。 + +- **401 Unauthorized** :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。 + +- **403 Forbidden** :请求被拒绝。 + +- **404 Not Found** + +## 5XX 服务器错误 + +- **500 Internal Server Error** :服务器正在执行请求时发生错误。 + +- **503 Service Unavailable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 + +# 四、HTTP 首部 + +有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。 + +各种首部字段及其含义如下(不需要全记,仅供查阅): + +## 通用首部字段 + +| 首部字段名 | 说明 | +| :--: | :--: | +| Cache-Control | 控制缓存的行为 | +| Connection | 控制不再转发给代理的首部字段、管理持久连接| +| Date | 创建报文的日期时间 | +| Pragma | 报文指令 | +| Trailer | 报文末端的首部一览 | +| Transfer-Encoding | 指定报文主体的传输编码方式 | +| Upgrade | 升级为其他协议 | +| Via | 代理服务器的相关信息 | +| Warning | 错误通知 | + +## 请求首部字段 + +| 首部字段名 | 说明 | +| :--: | :--: | +| Accept | 用户代理可处理的媒体类型 | +| Accept-Charset | 优先的字符集 | +| Accept-Encoding | 优先的内容编码 | +| Accept-Language | 优先的语言(自然语言) | +| Authorization | Web 认证信息 | +| Expect | 期待服务器的特定行为 | +| From | 用户的电子邮箱地址 | +| Host | 请求资源所在服务器 | +| If-Match | 比较实体标记(ETag) | +| If-Modified-Since | 比较资源的更新时间 | +| If-None-Match | 比较实体标记(与 If-Match 相反) | +| If-Range | 资源未更新时发送实体 Byte 的范围请求 | +| If-Unmodified-Since | 比较资源的更新时间(与 If-Modified-Since 相反) | +| Max-Forwards | 最大传输逐跳数 | +| Proxy-Authorization | 代理服务器要求客户端的认证信息 | +| Range | 实体的字节范围请求 | +| Referer | 对请求中 URI 的原始获取方 | +| TE | 传输编码的优先级 | +| User-Agent | HTTP 客户端程序的信息 | + +## 响应首部字段 + +| 首部字段名 | 说明 | +| :--: | :--: | +| Accept-Ranges | 是否接受字节范围请求 | +| Age | 推算资源创建经过时间 | +| ETag | 资源的匹配信息 | +| Location | 令客户端重定向至指定 URI | +| Proxy-Authenticate | 代理服务器对客户端的认证信息 | +| Retry-After | 对再次发起请求的时机要求 | +| Server | HTTP 服务器的安装信息 | +| Vary | 代理服务器缓存的管理信息 | +| WWW-Authenticate | 服务器对客户端的认证信息 | + +## 实体首部字段 + +| 首部字段名 | 说明 | +| :--: | :--: | +| Allow | 资源可支持的 HTTP 方法 | +| Content-Encoding | 实体主体适用的编码方式 | +| Content-Language | 实体主体的自然语言 | +| Content-Length | 实体主体的大小 | +| Content-Location | 替代对应资源的 URI | +| Content-MD5 | 实体主体的报文摘要 | +| Content-Range | 实体主体的位置范围 | +| Content-Type | 实体主体的媒体类型 | +| Expires | 实体主体过期的日期时间 | +| Last-Modified | 资源的最后修改日期时间 | + +# 五、具体应用 + +## 连接管理 + +

+ +### 1. 短连接与长连接 + +当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问的 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。 + +长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。 + +- 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 `Connection : close`; +- 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 `Connection : Keep-Alive`。 + +### 2. 流水线 + +默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。 + +流水线是在同一条长连接上连续发出请求,而不用等待响应返回,这样可以减少延迟。 + +## Cookie + +HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 + +Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 + +Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。 + +### 1. 用途 + +- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) +- 个性化设置(如用户自定义设置、主题等) +- 浏览器行为跟踪(如跟踪分析用户行为等) + +### 2. 创建过程 + +服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。 + +```html +HTTP/1.0 200 OK +Content-type: text/html +Set-Cookie: yummy_cookie=choco +Set-Cookie: tasty_cookie=strawberry + +[page content] +``` + +客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。 + +```html +GET /sample_page.html HTTP/1.1 +Host: www.example.org +Cookie: yummy_cookie=choco; tasty_cookie=strawberry +``` + +### 3. 分类 + +- 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。 +- 持久性 Cookie:指定过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie。 + +```html +Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; +``` + +### 4. 作用域 + +Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。 + +Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配: + +- /docs +- /docs/Web/ +- /docs/Web/HTTP + +### 5. JavaScript + +浏览器通过 `document.cookie` 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。 + +```html +document.cookie = "yummy_cookie=choco"; +document.cookie = "tasty_cookie=strawberry"; +console.log(document.cookie); +``` + +### 6. HttpOnly + +标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。跨站脚本攻击 (XSS) 常常使用 JavaScript 的 `document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。 + +```html +Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly +``` + +### 7. Secure + +标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。 + +### 8. Session + +除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 + +Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 + +使用 Session 维护用户登录状态的过程如下: + +- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; +- 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; +- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; +- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 + +应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 + +### 9. 浏览器禁用 Cookie + +此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。 + +### 10. Cookie 与 Session 选择 + +- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选 Session; +- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; +- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 + +## 缓存 + +### 1. 优点 + +- 缓解服务器压力; +- 降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存服务器在地理位置上也有可能比源服务器来得近,例如浏览器缓存。 + +### 2. 实现方法 + +- 让代理服务器进行缓存; +- 让客户端浏览器进行缓存。 + +### 3. Cache-Control + +HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。 + +**3.1 禁止进行缓存** + +no-store 指令规定不能对请求或响应的任何一部分进行缓存。 + +```html +Cache-Control: no-store +``` + +**3.2 强制确认缓存** + +no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效时才能使用该缓存对客户端的请求进行响应。 + +```html +Cache-Control: no-cache +``` + +**3.3 私有缓存和公共缓存** + +private 指令规定了将资源作为私有缓存,只能被单独用户使用,一般存储在用户浏览器中。 + +```html +Cache-Control: private +``` + +public 指令规定了将资源作为公共缓存,可以被多个用户使用,一般存储在代理服务器中。 + +```html +Cache-Control: public +``` + +**3.4 缓存过期机制** + +max-age 指令出现在请求报文,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。 + +max-age 指令出现在响应报文,表示缓存资源在缓存服务器中保存的时间。 + +```html +Cache-Control: max-age=31536000 +``` + +Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。 + +```html +Expires: Wed, 04 Jul 2012 08:26:05 GMT +``` + +- 在 HTTP/1.1 中,会优先处理 max-age 指令; +- 在 HTTP/1.0 中,max-age 指令会被忽略掉。 + +### 4. 缓存验证 + +需要先了解 ETag 首部字段的含义,它是资源的唯一标识。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一标识。 + +```html +ETag: "82e22293907ce725faf67773957acd12" +``` + +可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。 + +```html +If-None-Match: "82e22293907ce725faf67773957acd12" +``` + +Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有实体主体的 304 Not Modified 响应报文。 + +```html +Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT +``` + +```html +If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT +``` + +## 内容协商 + +通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。 + +### 1. 类型 + +**1.1 服务端驱动型** + +客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language,服务器根据这些字段返回特定的资源。 + +它存在以下问题: + +- 服务器很难知道客户端浏览器的全部信息; +- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术); +- 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。 + +**1.2 代理驱动型** + +服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。 + +### 2. Vary + +```html +Vary: Accept-Language +``` + +在使用内容协商的情况下,只有当缓存服务器中的缓存满足内容协商条件时,才能使用该缓存,否则应该向源服务器请求该资源。 + +例如,一个客户端发送了一个包含 Accept-Language 首部字段的请求之后,源服务器返回的响应包含 `Vary: Accept-Language` 内容,缓存服务器对这个响应进行缓存之后,在客户端下一次访问同一个 URL 资源,并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。 + +## 内容编码 + +内容编码将实体主体进行压缩,从而减少传输的数据量。 + +常用的内容编码有:gzip、compress、deflate、identity。 + +浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级。服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,响应报文的 Vary 首部字段至少要包含 Content-Encoding。 + +## 范围请求 + +如果网络出现中断,服务器只发送了一部分数据,范围请求可以使得客户端只请求服务器未发送的那部分数据,从而避免服务器重新发送所有数据。 + +### 1. Range + +在请求报文中添加 Range 首部字段指定请求的范围。 + +```html +GET /z4d4kWk.jpg HTTP/1.1 +Host: i.imgur.com +Range: bytes=0-1023 +``` + +请求成功的话服务器返回的响应包含 206 Partial Content 状态码。 + +```html +HTTP/1.1 206 Partial Content +Content-Range: bytes 0-1023/146515 +Content-Length: 1024 +... +(binary content) +``` + +### 2. Accept-Ranges + +响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求,可以处理使用 bytes,否则使用 none。 + +```html +Accept-Ranges: bytes +``` + +### 3. 响应状态码 + +- 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。 +- 在请求的范围越界的情况下,服务器会返回 416 Requested Range Not Satisfiable 状态码。 +- 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。 + +## 分块传输编码 + +Chunked Transfer Encoding,可以把数据分割成多块,让浏览器逐步显示页面。 + +## 多部分对象集合 + +一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。 + +例如,上传多个表单时可以使用如下方式: + +```html +Content-Type: multipart/form-data; boundary=AaB03x + +--AaB03x +Content-Disposition: form-data; name="submit-name" + +Larry +--AaB03x +Content-Disposition: form-data; name="files"; filename="file1.txt" +Content-Type: text/plain + +... contents of file1.txt ... +--AaB03x-- +``` + +## 虚拟主机 + +HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。 + +## 通信数据转发 + +### 1. 代理 + +代理服务器接受客户端的请求,并且转发给其它服务器。 + +使用代理的主要目的是: + +- 缓存 +- 负载均衡 +- 网络访问控制 +- 访问日志记录 + +代理服务器分为正向代理和反向代理两种: + +- 用户察觉得到正向代理的存在。 + +

+ +- 而反向代理一般位于内部网络中,用户察觉不到。 + +

+ +### 2. 网关 + +与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。 + +### 3. 隧道 + +使用 SSL 等加密手段,在客户端和服务器之间建立一条安全的通信线路。 + +# 六、HTTPS + +HTTP 有以下安全性问题: + +- 使用明文进行通信,内容可能会被窃听; +- 不验证通信方的身份,通信方的身份有可能遭遇伪装; +- 无法证明报文的完整性,报文有可能遭篡改。 + +HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。 + +通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 + +

+ +## 加密 + +### 1. 对称密钥加密 + +对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。 + +- 优点:运算速度快; +- 缺点:无法安全地将密钥传输给通信方。 + +

+ +### 2.非对称密钥加密 + +非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。 + +公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。 + +非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。 + +- 优点:可以更安全地将公开密钥传输给通信发送方; +- 缺点:运算速度慢。 + +

+ +### 3. HTTPS 采用的加密方式 + +HTTPS 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。(下图中的 Session Key 就是对称密钥) + +

+ +## 认证 + +通过使用 **证书** 来对通信方进行认证。 + +数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。 + +服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 + +进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。 + +

+ +## 完整性保护 + +SSL 提供报文摘要功能来进行完整性保护。 + +HTTP 也提供了 MD5 报文摘要功能,但不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生了篡改。 + +HTTPS 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。 + +## HTTPS 的缺点 + +- 因为需要进行加密解密等过程,因此速度会更慢; +- 需要支付证书授权的高额费用。 + +# 七、HTTP/2.0 + +## HTTP/1.x 缺陷 + +HTTP/1.x 实现简单是以牺牲性能为代价的: + +- 客户端需要使用多个连接才能实现并发和缩短延迟; +- 不会压缩请求和响应首部,从而导致不必要的网络流量; +- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。 + +## 二进制分帧层 + +HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。 + +

+ +在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。 + +- 一个数据流(Stream)都有一个唯一标识符和可选的优先级信息,用于承载双向信息。 +- 消息(Message)是与逻辑请求或响应对应的完整的一系列帧。 +- 帧(Frame)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。 + +

+ +## 服务端推送 + +HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。 + +

+ +## 首部压缩 + +HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。 + +HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。 + +不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。 + +

+ +# 八、HTTP/1.1 新特性 + +详细内容请见上文 + +- 默认是长连接 +- 支持流水线 +- 支持同时打开多个 TCP 连接 +- 支持虚拟主机 +- 新增状态码 100 +- 支持分块传输编码 +- 新增缓存处理指令 max-age + +# 九、GET 和 POST 比较 + +## 作用 + +GET 用于获取资源,而 POST 用于传输实体主体。 + +## 参数 + +GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。 + +因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 `中文` 会转换为 `%E4%B8%AD%E6%96%87`,而空格会转换为 `%20`。POST 参数支持标准字符集。 + +``` +GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1 +``` + +``` +POST /test/demo_form.asp HTTP/1.1 +Host: w3schools.com +name1=value1&name2=value2 +``` + +## 安全 + +安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 + +GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。 + +安全的方法除了 GET 之外还有:HEAD、OPTIONS。 + +不安全的方法除了 POST 之外还有 PUT、DELETE。 + +## 幂等性 + +幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。 + +所有的安全方法也都是幂等的。 + +在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。 + +GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的: + +``` +GET /pageX HTTP/1.1 +GET /pageX HTTP/1.1 +GET /pageX HTTP/1.1 +GET /pageX HTTP/1.1 +``` + +POST /add_row HTTP/1.1 不是幂等的,如果调用多次,就会增加多行记录: + +``` +POST /add_row HTTP/1.1 -> Adds a 1nd row +POST /add_row HTTP/1.1 -> Adds a 2nd row +POST /add_row HTTP/1.1 -> Adds a 3rd row +``` + +DELETE /idX/delete HTTP/1.1 是幂等的,即使不同的请求接收到的状态码不一样: + +``` +DELETE /idX/delete HTTP/1.1 -> Returns 200 if idX exists +DELETE /idX/delete HTTP/1.1 -> Returns 404 as it just got deleted +DELETE /idX/delete HTTP/1.1 -> Returns 404 +``` + +## 可缓存 + +如果要对响应进行缓存,需要满足以下条件: + +- 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。 +- 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。 +- 响应报文的 Cache-Control 首部字段没有指定不进行缓存。 + +## XMLHttpRequest + +为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest: + +> XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。 + +- 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。 +- 而 GET 方法 Header 和 Data 会一起发送。 + +# 参考资料 + +- 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014. +- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP) +- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn) +- [htmlspecialchars](http://php.net/manual/zh/function.htmlspecialchars.php) +- [Difference between file URI and URL in java](http://java2db.com/java-io/how-to-get-and-the-difference-between-file-uri-and-url-in-java) +- [How to Fix SQL Injection Using Java PreparedStatement & CallableStatement](https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement) +- [浅谈 HTTP 中 Get 与 Post 的区别](https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html) +- [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/) +- [HTTP (HyperText Transfer Protocol)](https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html) +- [Web-VPN: Secure Proxies with SPDY & Chrome](https://www.igvita.com/2011/12/01/web-vpn-secure-proxies-with-spdy-chrome/) +- [File:HTTP persistent connection.svg](http://en.wikipedia.org/wiki/File:HTTP_persistent_connection.svg) +- [Proxy server](https://en.wikipedia.org/wiki/Proxy_server) +- [What Is This HTTPS/SSL Thing And Why Should You Care?](https://www.x-cart.com/blog/what-is-https-and-ssl.html) +- [What is SSL Offloading?](https://securebox.comodo.com/ssl-sniffing/ssl-offloading/) +- [Sun Directory Server Enterprise Edition 7.0 Reference - Key Encryption](https://docs.oracle.com/cd/E19424-01/820-4811/6ng8i26bn/index.html) +- [An Introduction to Mutual SSL Authentication](https://www.codeproject.com/Articles/326574/An-Introduction-to-Mutual-SSL-Authentication) +- [The Difference Between URLs and URIs](https://danielmiessler.com/study/url-uri/) +- [Cookie 与 Session 的区别](https://juejin.im/entry/5766c29d6be3ff006a31b84e#comment) +- [COOKIE 和 SESSION 有什么区别](https://www.zhihu.com/question/19786827) +- [Cookie/Session 的机制与安全](https://harttle.land/2015/08/10/cookie-session.html) +- [HTTPS 证书原理](https://shijianan.com/2017/06/11/https/) +- [What is the difference between a URI, a URL and a URN?](https://stackoverflow.com/questions/176264/what-is-the-difference-between-a-uri-a-url-and-a-urn) +- [XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) +- [XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?](https://blog.josephscott.org/2009/08/27/xmlhttprequest-xhr-uses-multiple-packets-for-http-post/) +- [Symmetric vs. Asymmetric Encryption – What are differences?](https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences) +- [Web 性能优化与 HTTP/2](https://www.kancloud.cn/digest/web-performance-http2) +- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn) + + + + +# 微信公众号 + + +更多精彩内容将发布在微信公众号 CyC2018 上,你也可以在公众号后台和我交流学习和求职相关的问题。另外,公众号提供了该项目的 PDF 等离线阅读版本,后台回复 "下载" 即可领取。公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲" 即可领取。我基本是按照这个大纲来进行复习的,对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。 + + +
diff --git "a/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java IO.md" "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java IO.md" new file mode 100644 index 00000000..8fa2cf71 --- /dev/null +++ "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java IO.md" @@ -0,0 +1,631 @@ + +* [一、概览](#一概览) +* [二、磁盘操作](#二磁盘操作) +* [三、字节操作](#三字节操作) + * [实现文件复制](#实现文件复制) + * [装饰者模式](#装饰者模式) +* [四、字符操作](#四字符操作) + * [编码与解码](#编码与解码) + * [String 的编码方式](#string-的编码方式) + * [Reader 与 Writer](#reader-与-writer) + * [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容) +* [五、对象操作](#五对象操作) + * [序列化](#序列化) + * [Serializable](#serializable) + * [transient](#transient) +* [六、网络操作](#六网络操作) + * [InetAddress](#inetaddress) + * [URL](#url) + * [Sockets](#sockets) + * [Datagram](#datagram) +* [七、NIO](#七nio) + * [流与块](#流与块) + * [通道与缓冲区](#通道与缓冲区) + * [缓冲区状态变量](#缓冲区状态变量) + * [文件 NIO 实例](#文件-nio-实例) + * [选择器](#选择器) + * [套接字 NIO 实例](#套接字-nio-实例) + * [内存映射文件](#内存映射文件) + * [对比](#对比) +* [八、参考资料](#八参考资料) + + + +# 一、概览 + +Java 的 I/O 大概可以分成以下几类: + +- 磁盘操作:File +- 字节操作:InputStream 和 OutputStream +- 字符操作:Reader 和 Writer +- 对象操作:Serializable +- 网络操作:Socket +- 新的输入/输出:NIO + +# 二、磁盘操作 + +File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。 + +递归地列出一个目录下所有文件: + +```java +public static void listAllFiles(File dir) { + if (dir == null || !dir.exists()) { + return; + } + if (dir.isFile()) { + System.out.println(dir.getName()); + return; + } + for (File file : dir.listFiles()) { + listAllFiles(file); + } +} +``` + +从 Java7 开始,可以使用 Paths 和 Files 代替 File。 + +# 三、字节操作 + +## 实现文件复制 + +```java +public static void copyFile(String src, String dist) throws IOException { + FileInputStream in = new FileInputStream(src); + FileOutputStream out = new FileOutputStream(dist); + + byte[] buffer = new byte[20 * 1024]; + int cnt; + + // read() 最多读取 buffer.length 个字节 + // 返回的是实际读取的个数 + // 返回 -1 的时候表示读到 eof,即文件尾 + while ((cnt = in.read(buffer, 0, buffer.length)) != -1) { + out.write(buffer, 0, cnt); + } + + in.close(); + out.close(); +} +``` + +## 装饰者模式 + +Java I/O 使用了装饰者模式来实现。以 InputStream 为例, + +- InputStream 是抽象组件; +- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作; +- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 + +

+ +实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。 + +```java +FileInputStream fileInputStream = new FileInputStream(filePath); +BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream); +``` + +DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。 + +# 四、字符操作 + +## 编码与解码 + +编码就是把字符转换为字节,而解码是把字节重新组合成字符。 + +如果编码和解码过程使用不同的编码方式那么就出现了乱码。 + +- GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节; +- UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节; +- UTF-16be 编码中,中文字符和英文字符都占 2 个字节。 + +UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。 + +Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。 + +## String 的编码方式 + +String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。 + +```java +String str1 = "中文"; +byte[] bytes = str1.getBytes("UTF-8"); +String str2 = new String(bytes, "UTF-8"); +System.out.println(str2); +``` + +在调用无参数 getBytes() 方法时,默认的编码方式不是 UTF-16be。双字节编码的好处是可以使用一个 char 存储中文和英文,而将 String 转为 bytes[] 字节数组就不再需要这个好处,因此也就不再需要双字节编码。getBytes() 的默认编码方式与平台有关,一般为 UTF-8。 + +```java +byte[] bytes = str1.getBytes(); +``` + +## Reader 与 Writer + +不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。 + +- InputStreamReader 实现从字节流解码成字符流; +- OutputStreamWriter 实现字符流编码成为字节流。 + +## 实现逐行输出文本文件的内容 + +```java +public static void readFileContent(String filePath) throws IOException { + + FileReader fileReader = new FileReader(filePath); + BufferedReader bufferedReader = new BufferedReader(fileReader); + + String line; + while ((line = bufferedReader.readLine()) != null) { + System.out.println(line); + } + + // 装饰者模式使得 BufferedReader 组合了一个 Reader 对象 + // 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法 + // 因此只要一个 close() 调用即可 + bufferedReader.close(); +} +``` + +# 五、对象操作 + +## 序列化 + +序列化就是将一个对象转换成字节序列,方便存储和传输。 + +- 序列化:ObjectOutputStream.writeObject() +- 反序列化:ObjectInputStream.readObject() + +不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。 + +## Serializable + +序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。 + +```java +public static void main(String[] args) throws IOException, ClassNotFoundException { + + A a1 = new A(123, "abc"); + String objectFile = "file/a1"; + + ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile)); + objectOutputStream.writeObject(a1); + objectOutputStream.close(); + + ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile)); + A a2 = (A) objectInputStream.readObject(); + objectInputStream.close(); + System.out.println(a2); +} + +private static class A implements Serializable { + + private int x; + private String y; + + A(int x, String y) { + this.x = x; + this.y = y; + } + + @Override + public String toString() { + return "x = " + x + " " + "y = " + y; + } +} +``` + +## transient + +transient 关键字可以使一些属性不会被序列化。 + +ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。 + +```java +private transient Object[] elementData; +``` + +# 六、网络操作 + +Java 中的网络支持: + +- InetAddress:用于表示网络上的硬件资源,即 IP 地址; +- URL:统一资源定位符; +- Sockets:使用 TCP 协议实现网络通信; +- Datagram:使用 UDP 协议实现网络通信。 + +## InetAddress + +没有公有的构造函数,只能通过静态方法来创建实例。 + +```java +InetAddress.getByName(String host); +InetAddress.getByAddress(byte[] address); +``` + +## URL + +可以直接从 URL 中读取字节流数据。 + +```java +public static void main(String[] args) throws IOException { + + URL url = new URL("http://www.baidu.com"); + + /* 字节流 */ + InputStream is = url.openStream(); + + /* 字符流 */ + InputStreamReader isr = new InputStreamReader(is, "utf-8"); + + /* 提供缓存功能 */ + BufferedReader br = new BufferedReader(isr); + + String line; + while ((line = br.readLine()) != null) { + System.out.println(line); + } + + br.close(); +} +``` + +## Sockets + +- ServerSocket:服务器端类 +- Socket:客户端类 +- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。 + +

+ +## Datagram + +- DatagramSocket:通信类 +- DatagramPacket:数据包类 + +# 七、NIO + +新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。 + +## 流与块 + +I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。 + +面向流的 I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。 + +面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 + +I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。 + +## 通道与缓冲区 + +### 1. 通道 + +通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。 + +通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。 + +通道包括以下类型: + +- FileChannel:从文件中读写数据; +- DatagramChannel:通过 UDP 读写网络中数据; +- SocketChannel:通过 TCP 读写网络中数据; +- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 + +### 2. 缓冲区 + +发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。 + +缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。 + +缓冲区包括以下类型: + +- ByteBuffer +- CharBuffer +- ShortBuffer +- IntBuffer +- LongBuffer +- FloatBuffer +- DoubleBuffer + +## 缓冲区状态变量 + +- capacity:最大容量; +- position:当前已经读写的字节数; +- limit:还可以读写的字节数。 + +状态变量的改变过程举例: + +① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。 + +

+ +② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。 + +

+ +③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。 + +

+ +④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。 + +

+ +⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。 + +

+ +## 文件 NIO 实例 + +以下展示了使用 NIO 快速复制文件的实例: + +```java +public static void fastCopy(String src, String dist) throws IOException { + + /* 获得源文件的输入字节流 */ + FileInputStream fin = new FileInputStream(src); + + /* 获取输入字节流的文件通道 */ + FileChannel fcin = fin.getChannel(); + + /* 获取目标文件的输出字节流 */ + FileOutputStream fout = new FileOutputStream(dist); + + /* 获取输出字节流的文件通道 */ + FileChannel fcout = fout.getChannel(); + + /* 为缓冲区分配 1024 个字节 */ + ByteBuffer buffer = ByteBuffer.allocateDirect(1024); + + while (true) { + + /* 从输入通道中读取数据到缓冲区中 */ + int r = fcin.read(buffer); + + /* read() 返回 -1 表示 EOF */ + if (r == -1) { + break; + } + + /* 切换读写 */ + buffer.flip(); + + /* 把缓冲区的内容写入输出文件中 */ + fcout.write(buffer); + + /* 清空缓冲区 */ + buffer.clear(); + } +} +``` + +## 选择器 + +NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。 + +NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。 + +通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。 + +因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。 + +应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。 + +

+ +### 1. 创建选择器 + +```java +Selector selector = Selector.open(); +``` + +### 2. 将通道注册到选择器上 + +```java +ServerSocketChannel ssChannel = ServerSocketChannel.open(); +ssChannel.configureBlocking(false); +ssChannel.register(selector, SelectionKey.OP_ACCEPT); +``` + +通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。 + +在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类: + +- SelectionKey.OP_CONNECT +- SelectionKey.OP_ACCEPT +- SelectionKey.OP_READ +- SelectionKey.OP_WRITE + +它们在 SelectionKey 的定义如下: + +```java +public static final int OP_READ = 1 << 0; +public static final int OP_WRITE = 1 << 2; +public static final int OP_CONNECT = 1 << 3; +public static final int OP_ACCEPT = 1 << 4; +``` + +可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如: + +```java +int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; +``` + +### 3. 监听事件 + +```java +int num = selector.select(); +``` + +使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。 + +### 4. 获取到达的事件 + +```java +Set keys = selector.selectedKeys(); +Iterator keyIterator = keys.iterator(); +while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + if (key.isAcceptable()) { + // ... + } else if (key.isReadable()) { + // ... + } + keyIterator.remove(); +} +``` + +### 5. 事件循环 + +因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。 + +```java +while (true) { + int num = selector.select(); + Set keys = selector.selectedKeys(); + Iterator keyIterator = keys.iterator(); + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + if (key.isAcceptable()) { + // ... + } else if (key.isReadable()) { + // ... + } + keyIterator.remove(); + } +} +``` + +## 套接字 NIO 实例 + +```java +public class NIOServer { + + public static void main(String[] args) throws IOException { + + Selector selector = Selector.open(); + + ServerSocketChannel ssChannel = ServerSocketChannel.open(); + ssChannel.configureBlocking(false); + ssChannel.register(selector, SelectionKey.OP_ACCEPT); + + ServerSocket serverSocket = ssChannel.socket(); + InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888); + serverSocket.bind(address); + + while (true) { + + selector.select(); + Set keys = selector.selectedKeys(); + Iterator keyIterator = keys.iterator(); + + while (keyIterator.hasNext()) { + + SelectionKey key = keyIterator.next(); + + if (key.isAcceptable()) { + + ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel(); + + // 服务器会为每个新连接创建一个 SocketChannel + SocketChannel sChannel = ssChannel1.accept(); + sChannel.configureBlocking(false); + + // 这个新连接主要用于从客户端读取数据 + sChannel.register(selector, SelectionKey.OP_READ); + + } else if (key.isReadable()) { + + SocketChannel sChannel = (SocketChannel) key.channel(); + System.out.println(readDataFromSocketChannel(sChannel)); + sChannel.close(); + } + + keyIterator.remove(); + } + } + } + + private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException { + + ByteBuffer buffer = ByteBuffer.allocate(1024); + StringBuilder data = new StringBuilder(); + + while (true) { + + buffer.clear(); + int n = sChannel.read(buffer); + if (n == -1) { + break; + } + buffer.flip(); + int limit = buffer.limit(); + char[] dst = new char[limit]; + for (int i = 0; i < limit; i++) { + dst[i] = (char) buffer.get(i); + } + data.append(dst); + buffer.clear(); + } + return data.toString(); + } +} +``` + +```java +public class NIOClient { + + public static void main(String[] args) throws IOException { + Socket socket = new Socket("127.0.0.1", 8888); + OutputStream out = socket.getOutputStream(); + String s = "hello world"; + out.write(s.getBytes()); + out.close(); + } +} +``` + +## 内存映射文件 + +内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。 + +向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。 + +下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。 + +```java +MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024); +``` + +## 对比 + +NIO 与普通 I/O 的区别主要有以下两点: + +- NIO 是非阻塞的; +- NIO 面向块,I/O 面向流。 + +# 八、参考资料 + +- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002. +- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html) +- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html) +- [Java NIO 浅析](https://tech.meituan.com/nio.html) +- [IBM: 深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html) +- [IBM: 深入分析 Java 中的中文编码问题](https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html) +- [IBM: Java 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html) +- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499) +- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document) +- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html) + + + + +# 微信公众号 + + +更多精彩内容将发布在微信公众号 CyC2018 上,你也可以在公众号后台和我交流学习和求职相关的问题。另外,公众号提供了该项目的 PDF 等离线阅读版本,后台回复 "下载" 即可领取。公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲" 即可领取。我基本是按照这个大纲来进行复习的,对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。 + + +
diff --git "a/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java \345\237\272\347\241\200.md" "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java \345\237\272\347\241\200.md" new file mode 100644 index 00000000..dc943c68 --- /dev/null +++ "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java \345\237\272\347\241\200.md" @@ -0,0 +1,1446 @@ + +* [一、数据类型](#一数据类型) + * [基本类型](#基本类型) + * [包装类型](#包装类型) + * [缓存池](#缓存池) +* [二、String](#二string) + * [概览](#概览) + * [不可变的好处](#不可变的好处) + * [String, StringBuffer and StringBuilder](#string,-stringbuffer-and-stringbuilder) + * [String Pool](#string-pool) + * [new String("abc")](#new-string"abc") +* [三、运算](#三运算) + * [参数传递](#参数传递) + * [float 与 double](#float-与-double) + * [隐式类型转换](#隐式类型转换) + * [switch](#switch) +* [四、继承](#四继承) + * [访问权限](#访问权限) + * [抽象类与接口](#抽象类与接口) + * [super](#super) + * [重写与重载](#重写与重载) +* [五、Object 通用方法](#五object-通用方法) + * [概览](#概览) + * [equals()](#equals) + * [hashCode()](#hashcode) + * [toString()](#tostring) + * [clone()](#clone) +* [六、关键字](#六关键字) + * [final](#final) + * [static](#static) +* [七、反射](#七反射) +* [八、异常](#八异常) +* [九、泛型](#九泛型) +* [十、注解](#十注解) +* [十一、特性](#十一特性) + * [Java 各版本的新特性](#java-各版本的新特性) + * [Java 与 C++ 的区别](#java-与-c-的区别) + * [JRE or JDK](#jre-or-jdk) +* [参考资料](#参考资料) + + + +# 一、数据类型 + +## 基本类型 + +- byte/8 +- char/16 +- short/16 +- int/32 +- float/32 +- long/64 +- double/64 +- boolean/\~ + +boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。 + +- [Primitive Data Types](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html) +- [The Java® Virtual Machine Specification](https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf) + +## 包装类型 + +基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。 + +```java +Integer x = 2; // 装箱 调用了 Integer.valueOf(2) +int y = x; // 拆箱 调用了 X.intValue() +``` + +- [Autoboxing and Unboxing](https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html) + +## 缓存池 + +new Integer(123) 与 Integer.valueOf(123) 的区别在于: + +- new Integer(123) 每次都会新建一个对象; +- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。 + +```java +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 +``` + +valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。 + +```java +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +在 Java 8 中,Integer 缓存池的大小默认为 -128\~127。 + +```java +static final int low = -128; +static final int high; +static final Integer cache[]; + +static { + // high value may be configured by property + int h = 127; + String integerCacheHighPropValue = + sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); + if (integerCacheHighPropValue != null) { + try { + int i = parseInt(integerCacheHighPropValue); + i = Math.max(i, 127); + // Maximum array size is Integer.MAX_VALUE + h = Math.min(i, Integer.MAX_VALUE - (-low) -1); + } catch( NumberFormatException nfe) { + // If the property cannot be parsed into an int, ignore it. + } + } + high = h; + + cache = new Integer[(high - low) + 1]; + int j = low; + for(int k = 0; k < cache.length; k++) + cache[k] = new Integer(j++); + + // range [-128, 127] must be interned (JLS7 5.1.7) + assert IntegerCache.high >= 127; +} +``` + +编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。 + +```java +Integer m = 123; +Integer n = 123; +System.out.println(m == n); // true +``` + +基本类型对应的缓冲池如下: + +- boolean values true and false +- all byte values +- short values between -128 and 127 +- int values between -128 and 127 +- char in the range \u0000 to \u007F + +在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。 + +在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax=<size> 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。 + +[StackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123 +](https://stackoverflow.com/questions/9030817/differences-between-new-integer123-integer-valueof123-and-just-123) + +# 二、String + +## 概览 + +String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承) + +在 Java 8 中,String 内部使用 char 数组存储数据。 + +```java +public final class String + implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final char value[]; +} +``` + +在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 `coder` 来标识使用了哪种编码。 + +```java +public final class String + implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final byte[] value; + + /** The identifier of the encoding used to encode the bytes in {@code value}. */ + private final byte coder; +} +``` + +value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 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 不可变性天生具备线程安全,可以在多个线程中安全地使用。 + +[Program Creek : Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/) + +## String, StringBuffer and StringBuilder + +**1. 可变性** + +- String 不可变 +- StringBuffer 和 StringBuilder 可变 + +**2. 线程安全** + +- String 不可变,因此是线程安全的 +- StringBuilder 不是线程安全的 +- StringBuffer 是线程安全的,内部使用 synchronized 进行同步 + +[StackOverflow : String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder) + +## String Pool + +字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。 + +当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。 + +下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。 + +```java +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 中。 + +```java +String s5 = "bbb"; +String s6 = "bbb"; +System.out.println(s5 == s6); // true +``` + +在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。 + +- [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning) +- [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html) + +## new String("abc") + +使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。 + +- "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量; +- 而使用 new 的方式会在堆中创建一个字符串对象。 + +创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。 + +```java +public class NewStringTest { + public static void main(String[] args) { + String s = new String("abc"); + } +} +``` + +使用 javap -verbose 进行反编译,得到以下内容: + +```java +// ... +Constant pool: +// ... + #2 = Class #18 // java/lang/String + #3 = String #19 // abc +// ... + #18 = Utf8 java/lang/String + #19 = Utf8 abc +// ... + + public static void main(java.lang.String[]); + descriptor: ([Ljava/lang/String;)V + flags: ACC_PUBLIC, ACC_STATIC + Code: + stack=3, locals=2, args_size=1 + 0: new #2 // class java/lang/String + 3: dup + 4: ldc #3 // String abc + 6: invokespecial #4 // Method java/lang/String."":(Ljava/lang/String;)V + 9: astore_1 +// ... +``` + +在 Constant Pool 中,#19 存储这字符串字面量 "abc",#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。 + +以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。 + +```java +public String(String original) { + this.value = original.value; + this.hash = original.hash; +} +``` + +# 三、运算 + +## 参数传递 + +Java 的参数是以值传递的形式传入方法中,而不是引用传递。 + +以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中使指针引用其它对象,那么这两个指针此时指向的是完全不同的对象,在一方改变其所指向对象的内容时对另一方没有影响。 + +```java +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(); + } +} +``` + +```java +public class PassByValueExample { + public static void main(String[] args) { + Dog dog = new Dog("A"); + System.out.println(dog.getObjectAddress()); // Dog@4554617c + func(dog); + System.out.println(dog.getObjectAddress()); // Dog@4554617c + System.out.println(dog.getName()); // A + } + + private static void func(Dog dog) { + System.out.println(dog.getObjectAddress()); // Dog@4554617c + dog = new Dog("B"); + System.out.println(dog.getObjectAddress()); // Dog@74a14482 + System.out.println(dog.getName()); // B + } +} +``` + +如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。 + +```java +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"); + } +} +``` + +[StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?](https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value) + +## float 与 double + +Java 不能隐式执行向下转型,因为这会使得精度降低。 + +1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。 + +```java +// float f = 1.1; +``` + +1.1f 字面量才是 float 类型。 + +```java +float f = 1.1f; +``` + +## 隐式类型转换 + +因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。 + +```java +short s1 = 1; +// s1 = s1 + 1; +``` + +但是使用 += 或者 ++ 运算符可以执行隐式类型转换。 + +```java +s1 += 1; +// s1++; +``` + +上面的语句相当于将 s1 + 1 的计算结果进行了向下转型: + +```java +s1 = (short) (s1 + 1); +``` + +[StackOverflow : Why don't Java's +=, -=, *=, /= compound assignment operators require casting?](https://stackoverflow.com/questions/8710619/why-dont-javas-compound-assignment-operators-require-casting) + +## switch + +从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。 + +```java +String s = "a"; +switch (s) { + case "a": + System.out.println("aaa"); + break; + case "b": + System.out.println("bbb"); + break; +} +``` + +switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。 + +```java +// long x = 111; +// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum' +// case 111: +// System.out.println(111); +// break; +// case 222: +// System.out.println(222); +// break; +// } +``` + +[StackOverflow : Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java) + +# 四、继承 + +## 访问权限 + +Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。 + +可以对类或类中的成员(字段以及方法)加上访问修饰符。 + +- 类可见表示其它类可以用这个类创建实例对象。 +- 成员可见表示其它类可以用这个类的实例对象访问到该成员; + +protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。 + +设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。 + +如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。 + +字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 公有字段,如果在某个时刻,我们想要使用 int 存储 id 字段,那么就需要修改所有的客户端代码。 + +```java +public class AccessExample { + public String id; +} +``` + +可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。 + +```java +public class AccessExample { + + private int id; + + public String getId() { + return id + ""; + } + + public void setId(String id) { + this.id = Integer.valueOf(id); + } +} +``` + +但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。 + +```java +public class AccessWithInnerClassExample { + + private class InnerClass { + int x; + } + + private InnerClass innerClass; + + public AccessWithInnerClassExample() { + innerClass = new InnerClass(); + } + + public int getValue() { + return innerClass.x; // 直接访问 + } +} +``` + +## 抽象类与接口 + +**1. 抽象类** + +抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。 + +抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。 + +```java +public abstract class AbstractClassExample { + + protected int x; + private int y; + + public abstract void func1(); + + public void func2() { + System.out.println("func2"); + } +} +``` + +```java +public class AbstractExtendClassExample extends AbstractClassExample { + @Override + public void func1() { + System.out.println("func1"); + } +} +``` + +```java +// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated +AbstractClassExample ac2 = new AbstractExtendClassExample(); +ac2.func1(); +``` + +**2. 接口** + +接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。 + +从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。 + +接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。 + +接口的字段默认都是 static 和 final 的。 + +```java +public interface InterfaceExample { + + void func1(); + + default void func2(){ + System.out.println("func2"); + } + + int x = 123; + // int y; // Variable 'y' might not have been initialized + public int z = 0; // Modifier 'public' is redundant for interface fields + // private int k = 0; // Modifier 'private' not allowed here + // protected int l = 0; // Modifier 'protected' not allowed here + // private void fun3(); // Modifier 'private' not allowed here +} +``` + +```java +public class InterfaceImplementExample implements InterfaceExample { + @Override + public void func1() { + System.out.println("func1"); + } +} +``` + +```java +// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated +InterfaceExample ie2 = new InterfaceImplementExample(); +ie2.func1(); +System.out.println(InterfaceExample.x); +``` + +**3. 比较** + +- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。 +- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。 +- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。 +- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。 + +**4. 使用选择** + +使用接口: + +- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法; +- 需要使用多重继承。 + +使用抽象类: + +- 需要在几个相关的类中共享代码。 +- 需要能控制继承来的成员的访问权限,而不是都为 public。 +- 需要继承非静态和非常量字段。 + +在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。 + +- [Abstract Methods and Classes](https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html) +- [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/) +- [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface) + + +## super + +- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。应该注意到,子类一定会调用父类的构造函数来完成初始化工作,一般是调用父类的默认构造函数,如果子类需要调用父类其它构造函数,那么就可以使用 super 函数。 +- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。 + +```java +public class SuperExample { + + protected int x; + protected int y; + + public SuperExample(int x, int y) { + this.x = x; + this.y = y; + } + + public void func() { + System.out.println("SuperExample.func()"); + } +} +``` + +```java +public class SuperExtendExample extends SuperExample { + + private int z; + + public SuperExtendExample(int x, int y, int z) { + super(x, y); + this.z = z; + } + + @Override + public void func() { + super.func(); + System.out.println("SuperExtendExample.func()"); + } +} +``` + +```java +SuperExample e = new SuperExtendExample(1, 2, 3); +e.func(); +``` + +```html +SuperExample.func() +SuperExtendExample.func() +``` + +[Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html) + +## 重写与重载 + +**1. 重写(Override)** + +存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。 + +为了满足里式替换原则,重写有以下三个限制: + +- 子类方法的访问权限必须大于等于父类方法; +- 子类方法的返回类型必须是父类方法返回类型或为其子类型。 +- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。 + +使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。 + +下面的示例中,SubClass 为 SuperClass 的子类,SubClass 重写了 SuperClass 的 func() 方法。其中: + +- 子类方法访问权限为 public,大于父类的 protected。 +- 子类的返回类型为 ArrayList,是父类返回类型 List 的子类。 +- 子类抛出的异常类型为 Exception,是父类抛出异常 Throwable 的子类。 +- 子类重写方法使用 @Override 注解,从而让编译器自动检查是否满足限制条件。 + +```java +class SuperClass { + protected List func() throws Throwable { + return new ArrayList<>(); + } +} + +class SubClass extends SuperClass { + @Override + public ArrayList func() throws Exception { + return new ArrayList<>(); + } +} +``` + +在调用一个方法时,先从本类中查找看是否有对应的方法,如果没有查找到再到父类中查看,看是否有继承来的方法。否则就要对参数进行转型,转成父类之后看是否有对应的方法。总的来说,方法调用的优先级为: + +- this.func(this) +- super.func(this) +- this.func(super) +- super.func(super) + + +```java +/* + A + | + B + | + C + | + D + */ + + +class A { + + public void show(A obj) { + System.out.println("A.show(A)"); + } + + public void show(C obj) { + System.out.println("A.show(C)"); + } +} + +class B extends A { + + @Override + public void show(A obj) { + System.out.println("B.show(A)"); + } +} + +class C extends B { +} + +class D extends C { +} +``` + +```java +public static void main(String[] args) { + + A a = new A(); + B b = new B(); + C c = new C(); + D d = new D(); + + // 在 A 中存在 show(A obj),直接调用 + a.show(a); // A.show(A) + // 在 A 中不存在 show(B obj),将 B 转型成其父类 A + a.show(b); // A.show(A) + // 在 B 中存在从 A 继承来的 show(C obj),直接调用 + b.show(c); // A.show(C) + // 在 B 中不存在 show(D obj),但是存在从 A 继承来的 show(C obj),将 D 转型成其父类 C + b.show(d); // A.show(C) + + // 引用的还是 B 对象,所以 ba 和 b 的调用结果一样 + A ba = new B(); + ba.show(c); // A.show(C) + ba.show(d); // A.show(C) +} +``` + +**2. 重载(Overload)** + +存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。 + +应该注意的是,返回值不同,其它都相同不算是重载。 + +# 五、Object 通用方法 + +## 概览 + +```java + +public native int hashCode() + +public boolean equals(Object obj) + +protected native Object clone() throws CloneNotSupportedException + +public String toString() + +public final native Class getClass() + +protected void finalize() throws Throwable {} + +public final native void notify() + +public final native void notifyAll() + +public final native void wait(long timeout) throws InterruptedException + +public final void wait(long timeout, int nanos) throws InterruptedException + +public final void wait() throws InterruptedException +``` + +## equals() + +**1. 等价关系** + +Ⅰ 自反性 + +```java +x.equals(x); // true +``` + +Ⅱ 对称性 + +```java +x.equals(y) == y.equals(x); // true +``` + +Ⅲ 传递性 + +```java +if (x.equals(y) && y.equals(z)) + x.equals(z); // true; +``` + +Ⅳ 一致性 + +多次调用 equals() 方法结果不变 + +```java +x.equals(y) == x.equals(y); // true +``` + +Ⅴ 与 null 的比较 + +对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false + +```java +x.equals(null); // false; +``` + +**2. 等价与相等** + +- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 +- 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。 + +```java +Integer x = new Integer(1); +Integer y = new Integer(1); +System.out.println(x.equals(y)); // true +System.out.println(x == y); // false +``` + +**3. 实现** + +- 检查是否为同一个对象的引用,如果是直接返回 true; +- 检查是否是同一个类型,如果不是,直接返回 false; +- 将 Object 对象进行转型; +- 判断每个关键域是否相等。 + +```java +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() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。 + +```java +EqualExample e1 = new EqualExample(1, 1, 1); +EqualExample e2 = new EqualExample(1, 1, 1); +System.out.println(e1.equals(e2)); // true +HashSet 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`,编译器会自动进行这个优化。 + +```java +@Override +public int hashCode() { + int result = 17; + result = 31 * result + x; + result = 31 * result + y; + result = 31 * result + z; + return result; +} +``` + +## toString() + +默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。 + +```java +public class ToStringExample { + + private int number; + + public ToStringExample(int number) { + this.number = number; + } +} +``` + +```java +ToStringExample example = new ToStringExample(123); +System.out.println(example.toString()); +``` + +```html +ToStringExample@4554617c +``` + +## clone() + +**1. cloneable** + +clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。 + +```java +public class CloneExample { + private int a; + private int b; +} +``` + +```java +CloneExample e1 = new CloneExample(); +// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object' +``` + +重写 clone() 得到以下实现: + +```java +public class CloneExample { + private int a; + private int b; + + @Override + public CloneExample clone() throws CloneNotSupportedException { + return (CloneExample)super.clone(); + } +} +``` + +```java +CloneExample e1 = new CloneExample(); +try { + CloneExample e2 = e1.clone(); +} catch (CloneNotSupportedException e) { + e.printStackTrace(); +} +``` + +```html +java.lang.CloneNotSupportedException: CloneExample +``` + +以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。 + +应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。 + +```java +public class CloneExample implements Cloneable { + private int a; + private int b; + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} +``` + +**2. 浅拷贝** + +拷贝对象和原始对象的引用类型引用同一个对象。 + +```java +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(); + } +} +``` + +```java +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 +``` + +**3. 深拷贝** + +拷贝对象和原始对象的引用类型引用不同对象。 + +```java +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; + } +} +``` + +```java +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 +``` + +**4. clone() 的替代方案** + +使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。 + +```java +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]; + } +} +``` + +```java +CloneConstructorExample e1 = new CloneConstructorExample(); +CloneConstructorExample e2 = new CloneConstructorExample(e1); +e1.set(2, 222); +System.out.println(e2.get(2)); // 2 +``` + +# 六、关键字 + +## final + +**1. 数据** + +声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。 + +- 对于基本类型,final 使数值不变; +- 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。 + +```java +final int x = 1; +// x = 2; // cannot assign value to final variable 'x' +final A y = new A(); +y.a = 1; +``` + +**2. 方法** + +声明方法不能被子类重写。 + +private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。 + +**3. 类** + +声明类不允许被继承。 + +## static + +**1. 静态变量** + +- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。 +- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。 + +```java +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; + } +} +``` + +**2. 静态方法** + +静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。 + +```java +public abstract class A { + public static void func1(){ + } + // public abstract static void func2(); // Illegal combination of modifiers: 'abstract' and 'static' +} +``` + +只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。 + +```java +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 + } +} +``` + +**3. 静态语句块** + +静态语句块在类初始化时运行一次。 + +```java +public class A { + static { + System.out.println("123"); + } + + public static void main(String[] args) { + A a1 = new A(); + A a2 = new A(); + } +} +``` + +```html +123 +``` + +**4. 静态内部类** + +非静态内部类依赖于外部类的实例,而静态内部类不需要。 + +```java +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(); + } +} +``` + +静态内部类不能访问外部类的非静态的变量和方法。 + +**5. 静态导包** + +在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。 + +```java +import static com.xxx.ClassName.* +``` + +**6. 初始化顺序** + +静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。 + +```java +public static String staticField = "静态变量"; +``` + +```java +static { + System.out.println("静态语句块"); +} +``` + +```java +public String field = "实例变量"; +``` + +```java +{ + System.out.println("普通语句块"); +} +``` + +最后才是构造函数的初始化。 + +```java +public InitialOrderTest() { + System.out.println("构造函数"); +} +``` + +存在继承的情况下,初始化顺序为: + +- 父类(静态变量、静态语句块) +- 子类(静态变量、静态语句块) +- 父类(实例变量、普通语句块) +- 父类(构造函数) +- 子类(实例变量、普通语句块) +- 子类(构造函数) + + +# 七、反射 + +每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。 + +类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载,该方法会返回一个 Class 对象。 + +反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。 + +Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类: + +- **Field** :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段; +- **Method** :可以使用 invoke() 方法调用与 Method 对象关联的方法; +- **Constructor** :可以用 Constructor 的 newInstance() 创建新的对象。 + +**反射的优点:** + +* **可扩展性** :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。 +* **类浏览器和可视化开发环境** :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。 +* **调试器和测试工具** : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。 + +**反射的缺点:** + +尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。 + +* **性能开销** :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。 + +* **安全限制** :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。 + +* **内部暴露** :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。 + + +- [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html) +- [深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/) + +# 八、异常 + +Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error** 和 **Exception**。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种: + +- **受检异常** :需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复; +- **非受检异常** :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复。 + +

+ +- [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception) +- [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html) + +# 九、泛型 + +```java +public class Box { + // T stands for "Type" + private T t; + public void set(T t) { this.t = t; } + public T get() { return t; } +} +``` + +- [Java 泛型详解](http://www.importnew.com/24029.html) +- [10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693) + +# 十、注解 + +Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。 + +[注解 Annotation 实现原理与自定义注解例子](https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html) + +# 十一、特性 + +## Java 各版本的新特性 + +**New highlights in Java SE 8** + +1. Lambda Expressions +2. Pipelines and Streams +3. Date and Time API +4. Default Methods +5. Type Annotations +6. Nashhorn JavaScript Engine +7. Concurrent Accumulators +8. Parallel operations +9. PermGen Error Removed + +**New highlights in Java SE 7** + +1. Strings in Switch Statement +2. Type Inference for Generic Instance Creation +3. Multiple Exception Handling +4. Support for Dynamic Languages +5. Try with Resources +6. Java nio Package +7. Binary Literals, Underscore in literals +8. Diamond Syntax + +- [Difference between Java 1.8 and Java 1.7?](http://www.selfgrowth.com/articles/difference-between-java-18-and-java-17) +- [Java 8 特性](http://www.importnew.com/19345.html) + +## 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 等预处理命令从而实现条件编译。 + +[What are the main differences between Java and C++?](http://cs-fundamentals.com/tech-interview/java/differences-between-java-and-cpp.php) + +## JRE or JDK + +- JRE is the JVM program, Java application need to run on JRE. +- JDK is a superset of JRE, JRE + tools for developing java programs. e.g, it provides the compiler "javac" + +# 参考资料 + +- Eckel B. Java 编程思想[M]. 机械工业出版社, 2002. +- Bloch J. Effective java[M]. Addison-Wesley Professional, 2017. + + + + +# 微信公众号 + + +更多精彩内容将发布在微信公众号 CyC2018 上,你也可以在公众号后台和我交流学习和求职相关的问题。另外,公众号提供了该项目的 PDF 等离线阅读版本,后台回复 "下载" 即可领取。公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲" 即可领取。我基本是按照这个大纲来进行复习的,对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。 + + +
diff --git "a/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java \345\256\271\345\231\250.md" "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java \345\256\271\345\231\250.md" new file mode 100644 index 00000000..278510c8 --- /dev/null +++ "b/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/notes/Java \345\256\271\345\231\250.md" @@ -0,0 +1,1163 @@ + +* [一、概览](#一概览) + * [Collection](#collection) + * [Map](#map) +* [二、容器中的设计模式](#二容器中的设计模式) + * [迭代器模式](#迭代器模式) + * [适配器模式](#适配器模式) +* [三、源码分析](#三源码分析) + * [ArrayList](#arraylist) + * [Vector](#vector) + * [CopyOnWriteArrayList](#copyonwritearraylist) + * [LinkedList](#linkedlist) + * [HashMap](#hashmap) + * [ConcurrentHashMap](#concurrenthashmap) + * [LinkedHashMap](#linkedhashmap) + * [WeakHashMap](#weakhashmap) +* [参考资料](#参考资料) + + + +# 一、概览 + +容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 + +## Collection + +

+ +### 1. Set + +- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。 + +- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 + +- LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。 + +### 2. List + +- ArrayList:基于动态数组实现,支持随机访问。 + +- Vector:和 ArrayList 类似,但它是线程安全的。 + +- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 + +### 3. Queue + +- LinkedList:可以用它来实现双向队列。 + +- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。 + +## Map + +

+ +- TreeMap:基于红黑树实现。 + +- HashMap:基于哈希表实现。 + +- HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。 + +- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 + + +# 二、容器中的设计模式 + +## 迭代器模式 + +

+ +Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。 + +从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。 + +```java +List list = new ArrayList<>(); +list.add("a"); +list.add("b"); +for (String item : list) { + System.out.println(item); +} +``` + +## 适配器模式 + +java.util.Arrays#asList() 可以把数组类型转换为 List 类型。 + +```java +@SafeVarargs +public static List asList(T... a) +``` + +应该注意的是 asList() 的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。 + +```java +Integer[] arr = {1, 2, 3}; +List list = Arrays.asList(arr); +``` + +也可以使用以下方式调用 asList(): + +```java +List list = Arrays.asList(1, 2, 3); +``` + +# 三、源码分析 + +如果没有特别说明,以下源码分析基于 JDK 1.8。 + +在 IDEA 中 double shift 调出 Search EveryWhere,查找源码文件,找到之后就可以阅读源码。 + +## ArrayList + + +### 1. 概览 + +因为 ArrayList 是基于数组实现的,所以支持快速随机访问。RandomAccess 接口标识着该类支持快速随机访问。 + +```java +public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable +``` + +数组的默认大小为 10。 + +```java +private static final int DEFAULT_CAPACITY = 10; +``` + +

+ +### 2. 扩容 + +添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。 + +扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 + +```java +public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; + return true; +} + +private void ensureCapacityInternal(int minCapacity) { + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); + } + ensureExplicitCapacity(minCapacity); +} + +private void ensureExplicitCapacity(int minCapacity) { + modCount++; + // overflow-conscious code + if (minCapacity - elementData.length > 0) + grow(minCapacity); +} + +private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + // minCapacity is usually close to size, so this is a win: + elementData = Arrays.copyOf(elementData, newCapacity); +} +``` + +### 3. 删除元素 + +需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。 + +```java +public E remove(int index) { + rangeCheck(index); + modCount++; + E oldValue = elementData(index); + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, numMoved); + elementData[--size] = null; // clear to let GC do its work + return oldValue; +} +``` + +### 4. Fail-Fast + +modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 + +在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。 + +```java +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException{ + // Write out element count, and any hidden stuff + int expectedModCount = modCount; + s.defaultWriteObject(); + + // Write out size as capacity for behavioural compatibility with clone() + s.writeInt(size); + + // Write out all elements in the proper order. + for (int i=0; i