疯狂JAVA讲义笔记(转载)

来源:互联网 发布:自动化营销软件 编辑:程序博客网 时间:2024/05/18 01:29

Lirx_Tech的专栏
[译][疯狂Java]面向对象:不可变类
1. final只是保证被修饰的变量的值不可变!

1) 注意!保证的是值不可变,什么是值?对于一个引用来说,就是指针咯,即指针的值不变,即其指向不变,但是我还是可以通过这个指针来调用可以改变该对象成员的方法啊!!2) 即final如果修饰引用变量,只能保证该引用的指向不变,但不能保证该引用指向的对象不变,其指向的对象还是可以改变的!!3) 例如:final Person p = new Person("Tom");  p.setName("Peter");是允许的!!
  1. 不可变类:

    1) 不可变类是指创建该类的对象后,这些对象的数据成员都不能改变;

    2) 这种需求是普遍的,例如Java的8个包装器类型以及String类都是不可变类;

    3) 那该如何设计自己的不可变类呢?三原则:

     i. 数据成员都是private final的!这是显然的; ii. 只提供getter,不能提供setter!这也是显然的; iii. hashCode和equals应该根据数据成员计算得来,并且hashCode相等的情况下也要保证equals相等;

    4) 有了以上三原则就基本能保证设计的类是不可变的了;

!!什么叫做基本?难道这样还不能保证是不可变类吗?

!!诚然,如果数据成员都是Integer、String这些本身就是不可变类,那当然没有问题,但往往业务上你想包含的数据成员都是其它公司开发的类库中的类,而很多类本身设计时都是可变的,但你往往有得不到这些类库的源码,因此你无法通过改动源码的方式把这些类改成不可变类;

 5) 因此现在的问题是,如果你想把一个包含可变类对象的类设计成不可变类应该如何做呢?      i. 其实思路很简单!无非就是从两个方面出发:         a. 所有在类内部对该可变成员的访问一定要保证不能对其进行修改。这是显而易见的!根本不用说都应该能想到;         b. 另外可能发生修改隐患的地方就是返回这个可变成员的地方了:如果一个方法返回的刚好就是这个可变成员,那么返回后在类的外部就完全有可能对该成员进行修改了(即返回数据成员也是一种暴露类的内部情况的一种情形);      ii. 因此需要做文章的地方就在于b.了;      iii. 其实应对策略也很简单,你只要创建一个该成员的副本并返回就行了,这样的话外界拿到的仅仅是该成员的副本,对副本修改不会影响到原本; 6) 示例:

class Test {
private final StringBuilder name; // 故意用StringBuilder,因为它是一个可变类
public Test(StringBuilder name) {
this.name = name;
}
public StringBuilder getName() {
return new StringBuilder(name); // 返回一个临时创建的副本,外界对副本的改变不会影响原本
}
}

作者:Lirx_Tech 发表于2016/6/26 17:22:42 原文链接
阅读:187 评论:0 查看评论
[译][疯狂Java]面向对象:final修饰符(修饰变量、方法、类)
1. final修饰变量:

1) 当final修饰变量时,其功能类似C++的const,有不可变的意思(类似常量),但是其用法比C++的const灵活地多,功能也更丰富;2) 用final可以修饰任何变量(成员变量、静态成员变量、局部变量等);3) final变量的三大规则:     i. 定义和初始化可以分开:final不像C++的const必须在定义变量的同时给出初始值,final的规则是:final变量一旦获得初始值就不可改变;

!!也就是说在定义一个final变量的时候可以不给定初始值,一旦在后面获得初始值后就不可改变(即定义final变量和给定初始值可以分开进行);

     ii. final变量的初始值不一定是编译时就能确定的值,也可以是运行时才确定的值!         a. C++的const变量要求必须用编译时就能确定的值来初始化,说白了就是用一个字面值(常量)对const变量进行初始化!         b. Java的final变量除了允许用编译时确定的值来初始化之外还允许用一个运行时才能确定的值来初始化final变量(例如用另一个变量去初始化final变量,也可以用一个方法的返回值来初始化final变量!)

public class Test {

private final int a;private int createVal(int val) {    return val + 17;}public Test(int val) { // 并且这里的final变量是定义和初始化分开的    this.a = createVal(val); // 彻彻底底地运行时才能确定的值来初始化final变量}


}
iii. 任何final变量系统都不会为其提供默认值初始化值(二进制0),也就是说必须要由用户自己显式地对final变量进行初始化!!否则就会编译报错!强制要求你显式初始化!

  1. final局部变量的初始化:系统同样不会隐式给出默认值,必须自己显式初始化

    1) 必须要在该局部变量的作用域内、其第一次使用之前完成对其显式初始化,否则就会编译报错,强制要求你初始化;

    2) 特例:final也可以修饰方法形参,而方法形参的初始化是方法调用实参传形参时隐式完成的,在这种情况下就无需再在方法体中对final形参初始化了!

!例如:public void func(final int a) { System.out.println(a); } // 无需再初始化,传参的过程就是隐式的初始化过程

  1. final成员变量的初始化:系统同样不会隐式给出默认值,必须自己显式初始化

    1) 对于静态成员,就必须在使用静态final成员之前必须完成初始化,因此必须在创建类时完成初始化,也就是说必须在静态初始化代码中完成初始化;

!!初始化必须在静态初始化块和直接默认值之间二选一,不能重复,这是显然的;

2) 对于非静态成员,也必须在使用该成员之前完成初始化,因此必须在创建对象的时候完成初始化,也就是说必须在初始化代码或构造器中完成初始化;

!!初始化必须在直接默认值、初始化块、构造器中三选一,不能重复,这也是显然的!

  1. final宏:

    1) 如果一个变量符合这三个条件,那它既是一个final宏(即一种特殊的final变量):

     i. 是一个final变量; ii. 定义变量时就初始化; iii. 初始化值是编译时就可确定的(即用一个常量初始化);

    2) 那么该变量出现的地方都会用其初始化它的那个常量进行宏替换(该常量肯定放在常量池中管理)!

    3) 例如:final int a = 5; // 后面所有出现a的地方都用字面值5替换(编译之前预处理之后进行宏替换);

    4) 只要是编译时就能确定的就行,例如:final int a = 5 + 1; final String s = “lala” + “haha”; 或者final int a = 5; final String s = a + “lala”; // 其中a会进行宏替换成5,因此s的初始化值也是编译时就能确定的!

    5) 如果用一个非final宏的变量或者任何函数调用来初始化就不是编译时就能确定了,此时的final变量就不是一个final宏!

    6) 一个典型示例:

String a = “lalahaha”;
String b = “lala”;
String c = “haha”;
String d = b + c;
out.println(a == d); // false

String a = “lalahaha”;
final String b = “lala”;
final String c = “haha”;
String d = b + c;
out.println(a == d); // true

  1. final方法:

    1) 用final修饰过的方法表示不能被子类重写,如果重写则编译报错!

    2) 但不过对于父类的private方法,由于其子类是不可见的,因此子类即使“重写”,那也是定义了一个新的方法而已,因此子类可以“重写”父类的final方法(其实是定义了一个新的方法而已);

    3) final方法是指不能被子类重写,但是仍然可以被重载的!!

!!重载是平行的,重写是垂直的;

4) Object的getClass方法就是final方法,是一种即为基础的通用方法(是一个调用C语言的本地方法);
  1. final类:被final修饰过的类表示不能被继承!否则会编译报错!!

作者:Lirx_Tech 发表于2016/6/26 16:52:04 原文链接
阅读:130 评论:0 查看评论
[译][疯狂Java]面向对象:单例模式
1. 单例模式的应用:

1) 单例类就是指该类最多只能创建一个对象,不能创建超过一个的对象;2) 这样做的原因:要么就是为了降低系统开销,要么这种类型的对象创建了多个没有任何意义,最多只需要一个就行,例如窗口管理器、假脱机打印设备、数据库引擎访问结点;
  1. 单例模式的模板:

    1) 这种模式下构造器的使用肯定是受限的!因为不能让外界自由使用,否则就能创建多个对象了,因此构造器一定是private的!

    2) 由于只有静态数据成员只会保存一份(对象数据成员是跟随对象保存的,有几个对象就有几份对象数据成员),因此单例模式的单例对象也必须是静态的;

    3) 虽然构造器是private的,外界不能访问,但你尽管是单例,那这个单例外界还是需要使用的,否则没有任何意义,因此必须要提供若干静态方法来访问这个静态的单例对象;

    4) 模板就是根据上面这三条原则设计的:

class Singleton {
private Singleton() {} // 私有的构造器

private static Singleton instance;  // 单例对象设置为静态,这样就只能保存一份了public static Singleton getInstance() { // 使用静态方法来访问该单例对象    if (instance == null) {        instance = new Singleton(); // 自己内部是可以访问私有的构造器的    }    return instance;}

}

作者:Lirx_Tech 发表于2016/6/26 16:00:47 原文链接
阅读:114 评论:0 查看评论
[译][疯狂Java]面向对象:常量池、equals标准模板
1. Java的常量池:

1) 一般来说,常量对一个编程语言来说就是指编译时就可以确定的值,说白了就是字面值;2) 例如数字字面值5、1.3、5L、0xAFF,字符串字面值"lala"等;3) 和C/C++管理常量不同的是(C/C++直接将常量编译到代码段里了,例如b = val + 5就直接编译成add b, val, 5,这个5直接出现在代码段中),Java使用常量池来管理常量;4) Java常量池更加严谨和高效:     i. 编译的时候先扫描出所有程序中用到的常量,然后把他们放在同一的一段内存空间中存放,即常量池;     ii. 并且所有值相等的常量只保存一份(减少内存消耗),所有在代码中对常量的引用都链接到常量池中相应常量的地址;5) 示例:

String s1 = “1234”;
String s2 = “12”;
String s3 = “34”;
String s4 = “12” + “34”; // 虽然是相加后的结果,但是相加的结果也是能在编译时确定的,就是等于”1234”,因此可以在常量池中找到
out.println(s1 == s4); // true
out.println(s1 == new String(“1234”)); // false,一个在常量池,一个在堆中
out.println(s1 == s2 + s3); // false,所有使用变量相加的结果都是编译时无法确定的(运行时确定),因此不会从常量池里找,而是直接new一个隐藏对象保存相加结果

  1. equals方法的重写模板:

@Override
public boolean equals(Object obj) {
if (this == obj) { // 先比较地址
return true;
}

if (obj != null && obj.getClass() == ThisType.class) { // other不为空,且类型严格相同再比较值    ThisType other = (ThisType)obj;    if (值相等逻辑成立) {        return true;    }}return false;

}
!!equals的正规定义就是:自反性、对称性、传递性,在值满足这三个性质的同时,类型也要满足这三个性质,因此这里使用了obj.getClass() == ThisType.class的严格类型相等,而不是使用obj instanceof ThisType!
作者:Lirx_Tech 发表于2016/6/26 11:04:23 原文链接
阅读:138 评论:0 查看评论
[译][疯狂Java]面向对象:基本类型的包装器类型、自动装箱/拆箱、包装器类型大小/相等比较
1. 基本类型的包装器类型:

1) Java并不是严格的面向对象语言就是因为存在8中基本类型数据(int、char、double、boolean等),它们并不是类对象;2) 提供这8中基本类型主要是为了照顾C程序员的传统习惯;3) 为了让这8中基本类型也能面向对象,Java为其提供了包装器类型,名称都是各个基本类型的首字母大写(Long、Double、Float、Byte、

Short等),而int和char比较特殊,其包装器类型是Integer和Character,需要单独记忆;

4) 现在,这些包装器类型都是面向对象的类了,具有对象方法可以进行很多高级的操作,而不是简单的基本类型,都是引用类型了;
  1. 自动装箱/拆箱:

    1) JDK 1.5之前,基本类型数据和包装器类型数据之间相互转换、赋值非常麻烦,需要很多方法来实现,但现在Java提供了自动装箱和拆箱功能,使这个过程非常方便;

    2) 自动装箱:可以直接将基本类型数据赋给其对应的包装器类型数据,底层自动隐式地包装成包装器类型数据,例如,Integer a = 5;

     i. 注意,自动装箱一定要类型严格匹配!例如:Integer a = 5.5;错误,5.5是double,和Integer冲突!编译报错! ii. 即使是符合基本类型范围从小到大的规则(byte < char < int < long < float < double)也不能自动装箱,例如:Double d = 5;也不行!!5是int,和Double类型冲突!! iii. 即自动装箱的类型必须是严格的类型对应,即一个基本类型数值只能赋给其对应的包装器类型才行; iv. 本质就是,赋值号左边是面向对象的类型,而包装器类型没有基本类型的从小到大范围之说!!

    3) 自动拆箱:和自动装箱过程相反,可以直接把一个包装器类型赋给一个基本类型变量,例如,int a = new Integer(5);

     i. 由于自动拆箱过程中,赋值号左边的是基本类型,而基本类型是有从小到大范围之说的,因此自动拆箱可以类型不严格匹配,但只要符合基本类型的从小到大范围即可; ii. 例如:double d = new Integer(5);   // double范围比int大,所以可以这样自动拆箱 iii. 自动拆箱的原理就是先取出包装器对象中的基本类型值,然后再赋给相应的基本类型,因此上面的式子中,等号左边其实是先拆箱获取其基本类型数值(即int val = 5),然后再将这个val赋给double d;

    4) 只要是赋值的地方都可以发生自动装箱/拆箱:如方法的参数传递也是一种赋值的过程(值传递),因此也支持自动装箱/拆箱,例如

void func(int a); // obj.func(new Integer(5); 自动拆箱
void func(Integer a); // obj.func(5); 自动装箱

  1. 包装器的构造器、字符串和数值之间的相互转换:

    1) 包装器的构造器有两个版本:

     i. Type(type val);   // 以基本类型作为参数来构造相应的包装器类型 ii. Type(String val);  // 将一个字符串解析成相应的包装器类型

!例如:Integer a = new Integer(5); Integer a = new Integer(“5”);

!!其实,以基本类型作为参数来构造隐含了另一种重载版本,由于自动拆箱的存在,你可以用包装器类型对象来构造另一个包装器对象,因为从Type -> type是自动拆箱的过程,例如:

Integer a = new Integer(new Integer(5)); // 并不是Java重载了一个接受包装器类型的构造器版本,而是自动拆箱的原因,会先将Integer(5)拆箱成5,然后调用Integer(int val)构造器来构造!
2) 包装器的字符串参数版本其实就是一个将字符串转化成数值类型的方法,而各个包装器类型也提供了一个静态方法用于将字符串解析成基本数据类型:
public static type Type.parseType(String val); // type指代基本类型
!例如:public static int Integer.parseInt(String val); // 使用,int a = Integer.parseInt(“5”);
!!注意,使用构造器解析得到的是包装器类型,使用parse静态工具方法解析得到的是基本类型;

!!!要根据需要的返回值的类型(是包装器类型还是基本类型)来决定使用哪种解析方式;

3) 同理String也提供了一系列静态方法将基本类型数据解析成字符串:valueOf方法     i. 常用的单数据解析:public static String.valueOf(type val);  //  type指代基本类类型

!例如:public static String.valueOf(int val); // 使用,String s = String.valueOf(5);

     ii. 将char数组连接成字符串:public static String.valueOf(char[] data[, int offset, int count]);4) 其实最简单的数值转换成字符串的方法:     i. 所有的包装器类型都重写了toString方法,因此toString可以直接得到解析后的字符串;     ii. 基本类型支持自动装箱;     iii. 以上两点就决定了数值类型转换成字符串类型可以这样转换:和一个字符串相加即可!

!!例如:

Integer a = new Integer(5); a + “”; // 调用toString后相加
int a = 5; -> a + “”; // 以下两个都是先将a自动装箱成包装器类型,然后调用toString后相加

5 + “”;

  1. 数值型和字符串型相互转化的方式总结:

    1) 数值 -> 字符串:

     i. 基本类型  ->  字符串:String.valueOf ii. 包装器类型   ->  字符串:还是String.valueOf,因为可以自动拆箱 iii. 通用方法:将数值和""相加即可(两种类型都适用)

    2) 字符串 -> 数值:

     i. 字符串   ->  基本类型:Type.parseType ii. 字符串  -> 包装器类型:包装器类型的构造器
  2. 有了包装器类型就可以使用instanceof运算符了:

    1) 示例:

Object o = 5;
if (o instanceof Integer) {
int a = (Integer)o;
}
2) 小结:把基本类型赋给其对应的包装器类型的父类也是可以自动装箱的!!
3) 但是上面如果改成int a = o;就会编译报错!

!小结:把运行时类型为包装器类型但编译时类型不会其对应的基本类型的的变量赋给对应的基本类型是不会自动拆箱的!!会直接编译报错!!

  1. 数值型的大小比较:用符号比较、用包装器类型的静态工具方法比较

    1) 用符号比较:即>、<、>=、<=比较运算符对数值型(基本类型、包装器类型)比较;

     i. 符号两边可以是任意数值型的基本类型或包装器类型,两种类型可以混用; ii. 例如:5.5 > new Integer(5)等; iii. 比较运算符的规则:如果两边有包装器类型,那就先自动拆箱成基本类型,然后再进行比较; iv. 我们都知道,比较运算符是可以混合比较各种类型的数值的(比如double和int比,byte和char比等等);

    2) 由于符号比较比较混乱,允许混合类型比较,但有时需要在类型严格匹配的情况下进行比较使代码更加严谨,而各个包装器类型都提供了一个compare静态工具方法,可以保证在类型一致的情况下进行比较:

      i. 原型:public static int Type.compare(type x, type y);  ii. x<y返回-1,x == y返回0,否则返回1;  iii. Type是包装器类型,type是Type对应的基本类型;  iv. 由于自动拆箱,因此也可以传包装器类型;
  2. 相等比较:

    1) 直接用包装器对象的equals方法比较完全没有任何问题,通常更推荐用这种方法比较,不会出现任何错误;

    2) 使用==比较:

     i. 如果==一边是基本类型一边是包装器类型,那么包装器类型会自动拆箱成其对应的基本类型进行比较;

!!在这种情况下就变成了纯的基本类型之间的比较,而基本类型的==比较是允许混合类型的,因此允许这样写:6.0 == new Integer(6);

!!实质比较的是基本类型数值;

     ii. 如果==两边都是包装器类型,那就是纯引用类型之间的==比较了,那==比较的是两者的地址(是否指向同一个对象);         a. 和instanceof一样,两边的编译类型至少要具有父子关系才行,不能离谱到任意类型,比如new String() == new Date(),这两者用脚趾头想都是不可能相等的,因此直接编译报错!         b. 由于是比较两者的地址,因此像这样的比较返回的是false:new Integer(5) == new Integer(5),因为是两个不同的对象,地址不同;         c. 但是由于自动装箱的存在可能会出现一些令人费解的意外:

Integer a = 5;
Integer b = 5;
out.println(a == b); // true

Integer x = 128;
Integer y = 128;
out.println(x == y); // false
!!这其实是由自动装箱的算法决定的:由于-128-127(char范围)的整数太常用了(根据经验),因此Integer在静态初始化代码中创建了一块缓存,将-128-127包装成Integer存在这个缓存中,如果是通过自动装箱并且范围是-128-127得到的Integer就直接指向该缓存中的对象,因此上面两个符合这个标准,所以指向的是缓存区的同一个对象,而后两个由于128超出了这个范围,所以相当于使用new重新创建了两个对象,因此地址不同,比较结果为false;
!!这个道理告诉我们,包装器对象之间的相等比较一定要用equals对象方法!!!

作者:Lirx_Tech 发表于2016/6/25 18:00:40 原文链接
阅读:149 评论:0 查看评论
[译][疯狂Java]面向对象:构造器(this、super)
1. 构造器基本概念:

1) Java构造器和C++基本一致;2) 主要特点:     i. 名称和类名一样;     ii. 不得有返回类型(其默认返回构造的对象本身(this)),如果你强行加返回类型,系统就会把它当做一个普通的方法了,必须通过引用来调用,而不是用new来调用了;     iii. 会在初始化代码执行之后再执行:其实构造器执行的时间点已经很晚了,创建一个对象时会先为对象分配内存空间,然后执行初始化代码,最后再执行构造器;     iv. 和普通方法一样可以重载;     v. 访问控制符任意,特殊情况下可以用private限定,这就组织了类外部使用该构造器创建对象,一般是为了设计单例模式等;     vi. 构造器是无论如何必须要有的,如果用户不自己定义,Java就会隐式提供一个无参的、空的构造器,但一旦用户自己定义了一个构造器(只要有定义),Java就不会再提供那个默认的无参空的构造器了!3) 构造器的真正作用:     i. 初始化代码其实就是无参初始化行为,其作用是取代无参构造器;     ii. 那么构造器的真正作用就是有参初始化了,Java构造器就是为了接受参数,根据指定的参数在初始化代码的默认初始化基础上对成员数据进一步初始化;
  1. 在一个构造器中调用其它重载的构造器:

    1) 使用this调用,表示调用的是本对象中的其它构造器(不能是父类构造器,父类构造器无法继承);

    2) 例如:this(name, age);表示调用的是另一个重载的构造器:MyClass(String name, int age);

    3) 规则:

     i. 使用this调用其它版本的构造器的形式只能在构造器中使用,不能在其它方法中使用; ii. 如果不用this调用构造器,而是用类名来调用构造器就只能使用new运算符了,而new运算符是创建了一个新的对象,并非本身; iii. this调用其它构造器的语句必须是构造器的第一句,并且最多只能有一句!!
  2. 调用父类构造器:

    1) Java规定,初始化一个对象时必须要先初始化其父类部分,即一层一层追溯到Object为止;

    2) 这就意味着,Java也规定了一个类的构造器里,第一句必须是调用父类的构造器!!

     i. 但之前我们写的构造器代码中都没有在第一句调用父类构造器啊!! ii. 那是因为,如果第一句不是调用父类构造器代码也不是this调用其它构造器的话,那系统就隐式帮你调用父类无参的构造器(super()); iii. 如果第一句是this调用其它构造器,那么调用父类构造器的工作就交给this调用的那个重载构造器了;

!!这是一个递归定义;

3) 如果你不想使用默认的父类的无参构造器来初始化父类部分,就必须自己显示指定了:     i. 和this差不多,调用父类构造器使用super关键字;     ii. 例如:super(name, age);表示调用父类有参构造器,参数是String name和int age;

!注意,加了super限定后,调用的构造器就是父类的版本了!!

     iii. 和this一样,使用super调用父类构造器的代码必须是构造器的第一行,并且做多只能有一次,这就导致了不能同时使用super和this调用构造器;

!!这是正常的,因为使用this调用的其它重载构造器中也是会调用父类构造器的,因此同时使用super和this调用构造器功能上就重复了;

作者:Lirx_Tech 发表于2016/6/25 11:18:40 原文链接
阅读:185 评论:0 查看评论
[译][疯狂Java]面向对象:自发性多态问题
1. 自发性多态:

1) 自发性多态使用不当往往会造成意想不到的错误,所以编程时一定要注意!2) 先看示例:

class A {
void show() { // 该方法将会在子类中被重写
out.println(“A”);
}

void test() { // 在其中调用另一个会被子类重写的方法    show();}

}

class B extends A {
void show() { // 在子类中重写该方法
out.println(“B”);
}
}

public class Test {
public static void main(String[] args) {
new B().test(); // test没有被子类覆盖,因此这个test必定是那个唯一从父类继承来的test的
// 但其输出的却是B,即父类中test调用的却是子类重写的show!!
}
}
!发现输出的是B,即父类test调用的却是子类的版本!!即使this.show()这样调用也没有用!

3) 原因很简单,只要子类重写父类的方法,就会发生自发性多态:     i. 自发性多态:如果子类重写了父类的方法,那么在构造子类对象时,子类对象内部的super引用的虚函数表也会被覆盖!!     ii. 自发性多态是普遍的,即只要子类重写父类方法,那么构造子类对象时必定会发生自发性多态;     iii. 所以上面将的即使把test中的show调用改成this.show()也没用,因为在子类对象中这个this其实是super,而这个super的虚函数表已经被覆盖了,因此调用的是子类的重写版本;4) 自发性多态问题就是指上述问题,即在父类的方法中调用其它会被子类重写的方法,这个问题很严重,可能会导致严重错误,编程时一定要避免;5) 一个典型的由自发性多态问题引发的错误:在父类构造器中调用会被子类重写的方法

class A {
void test() {
out.println(“A test”);
}

A() {    test();}

}

class B extends A {
String name;
void test() {
name.length();
}
}

public class Test {
public static void main(String[] args) {
new B().test(); // 空指针异常
}
}
!!由于自发性多态,A的构造器调用的是B的test,而此时(在A的构造器中)B部分还未构造,因此这个时候B的name成员还不存在,因此在A的构造器了引用了并不存在的name,所以造成空指针异常!

作者:Lirx_Tech 发表于2016/6/24 17:53:07 原文链接
阅读:138 评论:0 查看评论
[译][疯狂Java]面向对象:初始化块、初始化代码、初始化顺序
1. 初始化块:

1) 在类中可以定义和方法平行的代码块,例如:public class A { { /* 初始化块 */ } ... },即直接用花括号括起来的代码块;2) 这种代码块就是初始化代码块,用于给数据成员初始化,里面可以调用方法,使用if、for等Java语句,也就是说初始化块本质上和普通方法没有区别;3) 初始化块会在构造器之前执行,对数据成员初始化,但它跟构造器和其它普通方法有区别的地方是,初始化块不能传入参数!这是非常明显的一个特征;     i. 这是个非常聪明的设计,如果需要对所有的对象全部进行完全相同的初始化处理那么初始化块就再合适不过了;     ii. 因为初始化块不接受任何参数,这就意味着初始化块中的代码永远是固定的,任何对象的初始化结果都是相同的,初始化块就是为了满足这样的需求;     iii. 初始化块是使编程更加简洁方便:         a. 不难看出初始化块完成的工作就是无参构造器的任务,那为什么不直接使用无参构造器还要设计一个初始化块呢?会不会多此一举呢?         b. 其实不是,考虑到如下情形:所有构造器都要先执行一段相同的无参初始化行为,为了防止代码冗余,在没有初始化块的情况下会把所有无参初始化行为都写在无参构造器中,然后在其它构造器的开始处调用这个无参构造器版本this();就行了;

!!但是现在有了初始化块,而初始化块都是在构造器之前执行的,因此现在就可以省略各个构造器开始的this()了;

!!跟何况有些构造器可能还想调用非无参版本的其它构造器,比如this(5, “lala”);之类的,但是每个构造器最多只能用一次this调用其它构造器,对于这种情况就又麻烦了,可能需要将无参初始化行为写在另外一个单独的方法中,然后再调用它了!

!!所以初始化块是一个非常非常成功的设计;

          c. 有了初始化块就只要提供给一个空的无参构造器即可!虽然初始化块可以完全取代无参构造器,但是养成良好的习惯,还是要提供一个的!
  1. 初始化代码:

    1) 还记得在定义数据成员的时候可以直接提供一个默认值吗?例如:private int m_data = 5;这种直接提供默认值其实也是一种初始化;

    2) 直接默认值和初始化块合并属于初始化代码,而构造器不属于初始化代码,构造器和初始化代码是两个平行的概念;

    3) 初始化代码的特性:

     i. 在构造器之前执行; ii. 初始化代码按照在源代码中的定义顺序依次执行(初始化块可以有多个),例如:

{ a = 6; }
private int a = 9;
private int b = 10;
{ a = 7; b = 11; }
!这个顺序很奇怪,初始化块有多个,并且初始化块可以放在数据成员定义之前(Java不像C++,需要前向引用声明,编译时会先识别所有的数据成员符号,然后再执行初始化代码);
!!实际初始化时会按照从上到下的顺序一条条执行,这里a先是6,再是9,最后再是7,因此最终结果是7,而b最终结果是11;

!!这里只是演示一下极端情况,为了说明初始化代码执行是按从上到下的顺序的,但实际编程中初始化代码只需要一个就行了,并且都是放在数据定义之后的,这样逻辑清晰并且美观;

4) 初始化代码的编写规范:     i. 具有显而易见的默认初始值的数据用直接默认值给出,例如刚创建的银行账号,余额必定是0,因此应该选择直接默认值初始化:private double account = 0.0;     ii. 对于需要使用一定逻辑(可以是非常复杂的逻辑、算法,特别是通过一个方法调用产生一个初始值)计算出初始值的数据则应该放在初始化块中:例如用随机数产生一个初始值;

!!由于某些方法可能会抛出异常,如果用这类方法产生初始值那就只能放在初始化块了,因为初始化块中可以加入异常处理逻辑,而直接默认值初始化显然不能解决抛出异常的问题,所以直接默认值初始化一般都是直接给出一个常量,很少使用一个方法来给定直接默认值的!

     iii. 使用直接默认值初始化的数据就不要再在初始化块中重复初始化了,即两者不要重复,各自的分工是明确的!
  1. 静态初始化块:

    1) 就是在普通初始化块之前加一个static,即static { /* 静态初始化代码 */ }

    2) 专门用来初始化静态数据成员的,而普通初始化块是用来初始化对象成员的;

    3) 同样,普通初始化块隐藏着一个this引用,所有的非局部变量默认使用this.引导,而静态初始化块没有隐藏的this引用,因此静态初始化块不能访问非静态成员!

    4) 静态初始化块和静态成员的直接默认初始化合并称为静态初始化代码,静态初始化代码会在类第一次加载时执行,会在所有对象创建之前执行,并且只执行一次;

  2. 一个完整的初始化过程的伪代码描述:

static_init() { // 静态成员初始化
if (当前类有父类) {
当前类的父类.static_init();
}
if (!当前类加载过) {
执行当前类的初始化代码;
}
}

init() { // 非静态成员初始化
static_init(); // 静态初始化最先执行

if (this有父类) {    super.init();}执行当前对象this的初始化代码;

}
!!这是一个递归的描述,因此下面示例:
class A {
static { out.println(“static A”); }
{ out.println(“A”); }
public A() { out.println(“cons A”); }
}

class B extends A {
static { out.println(“static B”); }
{ out.println(“B”); }
public B() { out.println(“cons B”); }
}

class C extends B {
static { out.println(“static C”); }
{ out.println(“C”); }
public C() { out.println(“cons C”); }
}

public class Test {
public static void main(String[] args) {
new C();
}
}
!!结果是:
static A
static B
static C
A
cons A
B
cons B
C
cons C
!!Java还是和其它语言保持一致的,即对一个对象初始化时一定要先保证其父类部分初始化再初始化自己的部分;

作者:Lirx_Tech 发表于2016/6/24 17:24:41 原文链接
阅读:169 评论:0 查看评论
[译][疯狂Java]面向对象:多态、编译时类型、运行时类型、向上兼容、兼容下反转、instanceof
1. 想明白多态必须先理解两个概念:编译时类型和运行时类型

1) 这两种类型都是针对引用变量的;2) 编译时类型:     i. 是声明引用变量时的类型;     ii. 例如:String s;  // 那么s的编译时类型就是String3) 运行时类型:     i. 是引用实际指向的对象的类型,和编译时类型完全没有任何关系;     ii. 例如:String s = new Father();  // s的运行是类型是由其指向的对象决定的,因此其运行时类型是Father

!!虽然上述编译不会通过,但这里只是举个极端的例子来演示;

     iii. 再例如:String a = new Father();  Son s = a;  // 经过两层链接后s的运行是类型还是Father,一定要看s最终指向的堆中的对象的类型是什么,这里s的编译时类型是Son,而运行时类型还是Father;

!!即运行时类型和编译时类型没有任何关系;

4) 一个引用只能访问到其编译时类型中的内容:     i. 一个引用能访问的范围是由其编译时类型决定的!     ii. 例如:String s = new Panel();  // 虽然编译不通过,但这里只是作为一个极端的例子来演示         a. 使用引用s只能访问String的数据成员和方法;         b. 即使s的运行时类型是Panel,但其运行时类型对于一个引用来说是不可见的;         c. s不能访问器运行时类型Panel中的任何数据和方法,只能通过s调用String的数据和方法;
  1. 多态:编译时类型表现出运行时类型的行为(虚函数表)

    1) 前面已经讲过了,一个引用变量只能调用其编译时类型类的方法;

    2) 其实Java底层为每个引用变量都准备了一张虚函数表,为什么说是虚函数,这些虚函数其实都是真正方法的入口地址;

    3) 虚函数表用于保存这个引用的编译时类型里所有可以访问的方法:

     i. 例如:Father类里两个public方法void f1();和void f2(); ii. 现在Father f;定义了一个编译时类型为Father的引用f; iii. 那么Java就会为f创建一张虚函数表,里面存放了Father类可以访问到的方法,即f1和f2;

    4) 那多态是什么?先看一下多态的本质,多态在底层的本质就是对引用的虚函数表进行覆盖:

     i. 接着上面的例子,现在是:Father f = new Son();     其中Son extends Father,并且Son覆盖了父类的方法f1; ii. 按照上面讲的知识点,还是会老样子,先为f建立一张虚函数表,里面存放的是Father范围内可见的方法f1和f2,而这里的f1仍然是父类中的f1; iii. 接着编译器检查到f的运行时类型是Son,并且Son是Father的子类,更重要的是这个子类还覆盖了Father的f1方法,接着多态就发生了,编译器将子类重写父类的方法f1覆盖掉了虚函数表中的f1; iv. 而编译时类型为Father的f只能调用其虚函数表中的方法,当调用到f1时就变成了子类覆盖的f1了!!

    5) 从上面的本质来看多态的要求:

     i. 最明显的就是引用的运行时类型必须是编译时类型的子类; ii. 只有在子类覆盖了父类方法时才会发生多态(因为只有这样才会修改引用的虚函数表);

!!只有i.不满足的情况下会编译报错;

!!可以看到多态是无需强制类型转换的(即运行时类型是编译时类型的子类时),上面无需:Father f = (Father)new Son();,因为这是向上兼容的!

6) 那多态有什么用呢?     i. 设想一个父类Father,有多个子类Son1、Son2、Son3...,每个子类都覆盖了f1方法,并且f1的内容还都不一样;     ii. 现在用一个Father的引用来指向各个不同子类的对象,然后分别调用f1,就会呈现出每个子类f1的行为;

!!每修改一次引用的指向,就会更新一次该引用所对应的虚函数表,例如f = new Xxx(),这里修改f的指向,因此就会根据新的运行时类型更新一下f的虚函数表;

     iii. 这就发生了,同一个引用,调用同一个方法却能表现出不同行为的现象,这就是多态;     iv. 而其本质就是编译时类型的引用表现出运行时类型的行为;
  1. 兼容下反转:

    1) 现在的问题是多态完事儿后想回来了应该怎么办?简单的来说就是,Father f = new Son();是多态,多态下的工作完成之后,我想让f恢复成Son类型(编译时类型),原因很简单,因为我现在又想使用f指向的对象的子类部分了(加入该Son对象只有一个f指向它,想要使用该对象的子类部分就只能通过f了);

    2) 总结来说,就是现在想让多态下引用的编译时类型恢复到和运行时类型相同的状态;

    3) 你可能会想,直接Son s = f;不就行了,现在s编译时类型是Son,而f指向的对象运行时类型是Son,那现在s的编译时类型不就和运行时类型想同了嘛!

     i. 虽然前面讲过,编译时类型和运行时类型没有关系,但上面的这个语句还是无法通过编译的; ii. 毕竟,Java是一个强类型语言,强类型语言指的是编译时对类型进行检查,而这个检查的对象就是编译时类型; iii. 编译器发现s的编译时类型是Son,而f的编译时类型是Father,而Father ≠ Son,因此会爆出类型转换异常!

    4) 向上转换是兼容的,向下转换是不兼容的!

     i. 前面讲过Father f = new Son(); 或者 Son s = new Son();  Father f = s;是不会发生类型转换异常的!因为等号左边的类型是左边的子类,即向上转换,即多态,因此类型是兼容的! ii. 而s = f之类的,即等号右边是左边的父类,即向下转换,是不兼容的,编译会直接报错,因为逻辑上就能解释,你只能说苹果是水果,但不能说水果是苹果,继承是is-a的关系;

    5) 但有时向下转换并不是错误的!就像上面的例子那样,多态下f的运行时类型是Son,把f赋值给Son引用恢复成Son编译时类型是无可厚非的,但这样做编译又会报错,此时就需要用到强制类型转换了!

     i. 强制类型转换除了可以用在数值类型的基本类型之间(int、double、char等之间的相互转换); ii. 其次强制类型转换还可以用在兼容情况下的引用类型反转,而这个说的就是上述的情况:     a. 例如:s = (Son)f;     b. 能这样使用的前提:         *1. 右边必须是左边的编译时类型范围更大(父类或间接父类);(兼容是必不可少的前提,而这个兼容指的是编译时类型的兼容,而这个方向是和向上转型相反看,因此叫做反转)         *2. 右边引用的运行时类型必须和左边引用的编译时类型相同;(这是强转的目的,即把编译时类型恢复成运行时类型)其实小于等于也行(即f的运行时类型是s编译时类型的子类,这样就又形成多态了) iii. 示例:Object o = "lala";  String s = (String)o;    // o的编译时类型是Object,而运行时类型是String,符合兼容反转的条件
  2. instanceof运算符:

    1) 该运算符和+、-、*之类的都属于Java的基本运算符;

    2) 该运算符的作用是对引用进行运行时类型检查;

    3) 用法:引用 instanceof 类/接口

    4) 目的是判断该引用的运行时类型是否为指定类或接口的子类或者相等,简单地说就是检查该引用指向的对象是否是指定类或接口的实例(或者其子类的实例),返回值当然是boolean类型的;

    5) 使用规定:

     i. instanceof左边的引用的编译时类型必须和操作符右边的类型有继承关系才行! ii. 例如:String a = "lala";  a instanceof Math就是错误的,直接编译报错!而a instanceof Object就对了,因为a的编译时类型String和Object具有继承关系; iii. 为什么有这样的规定呢?理由其实很好解释,那就要反问,为什么要使用instanceof操作符呢?不就是在进行类型强转之前先检查运行时类型是否匹配,如果不匹配就不强转(否则会报错)?那运行时类型匹配也是有前提,你设想,如果两个引用的编译时类型都没有继承关系,那他们运行时类型怎么可能会有继承关系呢??怎么可能匹配呢?用脚趾头都可以知道两个编译时类型无继承关系的引用是根本无法强转匹配的!

!!因此Java干脆要求做操作数的编译时类型必须至少要和右操作数有继承关系才能继续检查运行时类型;

6) 示例:

Object o = “lala”;
if (o instanceof String) { // true
String s = (String)o;
}
out.println(o instanceof Math); // false

String s = “lala”;
s instanceof Math; // 编译报错,s的编译时类型和Math没有继承关系

作者:Lirx_Tech 发表于2016/6/24 11:43:37 原文链接
阅读:105 评论:0 查看评论
[译][疯狂Java]面向对象:继承、覆盖、super
1. 类的继承:

1) Java使用extends关键字来继承一个类,例如:public class Son extends Father { ... }表示Son类继承Father类;2) Java不支持C++非常麻烦的多继承,即extends只能继承一个父类;3) Java使用extends来表示继承,从词义来看继承也是一种对父类的扩展,即子类可以获得父类的全部数据和方法(只不过有些对子类可见,有些不可见,要看父类中成员的访问控制符是什么,即使不可见也是用于父类的全部属性的,就是用不了(访问不了)而已),并且在此基础上还可以定义自己新增的数据和方法;
  1. 覆盖(重写)父类的方法:

    1) 子类可以通过重写(覆盖)父类的方法达到多态的目的(多态的概念下一章会详细介绍);

    2) 覆盖的原则必须要遵循“三同两小一大”:

     i. 三同:    a. 方法名相同;    b. 形参列表相同;    c. 覆盖的方法要和子类方法一样,要么都是静态的,要么都是非静态的,不能不一致,不一致会直接编译报错!

!!总结来说就是方法签名相同;

     ii. 两小:        a. 子类方法的返回值类型要比父类方法返回值类型的范围更小或者相等;(如果更小就是返回类型协变)        b. 子类方法抛出的异常的类型要比父类方法抛出的异常类型范围更小或者相等;

!!这在类型上是兼容的,可以理解为范围小的类型是范围大的类型的子类族,刚好向上兼容!

     iii. 一大:子类覆盖方法的访问控制符应该比父类的更大;

!!从大到小是:public -> protected -> default -> private

3) 只要符合“三同两小一大”原则的都会触发“覆盖(重写)”,这样Java编译器会把子类的该方法当成“覆盖”方法,将具备多态功能!只要上面的原则有一点不符,该方法就会被当做一个普通方法(或者是其它重载方法),不具备多态的特性;

!!再次强调,如果符合“两同两小一大”,但一个是静态一个是非静态(差一同),就会编译报错!千万要小心,静态属性必须相同;
4) 特殊情况:子类无法覆盖父类的private方法

     i. 由于父类的private方法对于子类来说也是不可见的;     ii. 既然子类不可见,那子类就会当完全没有这个方法;     iii. 因此如果子类还是强行覆盖该方法(访问控制符可以是任意一种,都是大于等于private的),那也不会形成覆盖!Java会把它当成一个属于子类的新方法;

!!因此private限定符要跟就不是用来覆盖的(继承、多态),仅仅表示这是一样非常私人的东西,谁都不能看见,包括自己的儿子;

小结:因此,对于父类中的private方法(静态、非静态)根本就不再“三同两小一大”的体系当中,因此子类中可以肆无忌惮地写和该方法同方法签名的方法,即使一个是静态的一个是非静态的也行,毕竟父类的private的方法子类是当完全不存在的!!

  1. 用super限定符来访问父类:

    1) 从父类中继承来的变量和方法是可以在子类中直接使用的(只要不是private的就行),比如从父类继承了一个int a,那么可以直接在子类中访问(使用)它,比如a + 5之类的直接用变量名来访问;

    2) 但如果子类自己的变量或方法和父类继承来的重名,那么对于变量会隐藏父类的变量(变量不会覆盖,父类子类各一份),对于方法也许会覆盖(如果符合“三同两小一大”的条件),这是因为在对象中所有对对象数据和方法的访问默认使用this.引导的,而this默认指代的是当前对象(this的类型是运行时类型),即子类对象,因此如果发生重名或覆盖,则调用的是子类的变量和方法(覆盖方法);

!!但有时又有想要调用父类继承的变量和方法的需求,这时就可以使用super限定符来访问父类部分了;

3) 和this引用一样,super也是引用:     i. this代表当前对象的引用,而super代表当前对象的父类部分的引用;     ii. this的运行时类型是子类,而super的运行时类型是父类,因此super只能访问父类部分,this可以访问整个子类(子类也包含父类),而如果发生重名或覆盖,用super来访问被覆盖和被重名的部分再合适不过了;     iii. 例如:super.a(访问父类被重名的数据)、super.func()(访问父类被覆盖的方法);4) super和this一样,都是非静态的,都是指代具体的对象,因此不能在静态方法和静态块中使用super限定符;5) super并不是只有在重名或覆盖的时候才可以用super,任何情况下都可以用super限定来访问父类部分;6) Java定位一个不加任何限定符的变量的顺序(例如一个变量a):是否是局部变量?  ->  是否是子类的变量?  ->  是否是父类的变量?   ->  一直追溯到Object;7) 由于super和this一样都指代对象,因此也可以用super来访问父类的静态变量,但这样做是不符合逻辑,尽量不要这样使用,还是用父类的类名来访问父类的静态变量;8) 也可以不使用super强行访问父类部分:     i. 就是利用向上转型的原理,向上转型是类型兼容的;     ii. 例如:Father类有一个a变量,Son也有一个自己定义的a,现在Son的对象是s,那么就可以这样访问父类部分的a,((Father)s).a;

!!即先对子类对象向上转型,转型后,运行时类型就变成父类了,这样就只有父类部分是可见的,因此访问的a也是父类部分的a;

作者:Lirx_Tech 发表于2016/6/23 21:59:58 原文链接
阅读:149 评论:0 查看评论
[译][疯狂Java]面向对象:封装(JavaBean)、包(package)、import(导入和静态导入)
1. 封装的关键——访问控制符:

1) Java有4种访问控制级别,范围从小到大(从严到宽)依次为:private、default(包)、protected、public2) 4中访问控制符的限定范围:     i. public:最宽松,对于一切都是可以访问的;     ii. private:最严,只有在当前类中可见,类外的一切都不可见(都无法访问),包括子类、外部类等一切都无法访问;     iii. default:包访问权限(什么控制符都不加默认就是包访问权限),即默认的访问权限,它对于包内是public,即包内的其它一切都能访问它,对于包外是不可见的;     iv. protected:子类访问权限,比default范围大一些(包含default),即包内是public的,而包外,只有其子类才能访问它(即包内public、包外只有对其子类public),使用protected通常是为了以后有子类继承它;3) 上述4中访问控制符都可以用于类的成员,而只有public和default才能用于外部类,外部类不能定义为private和protected,因为没有任何意义,例如对于private,类就是让人用的,如果定义为private那这个类就无法被人使用了,也就失去了任何意义,因此如果将外部类定义为private或者protected都将编译不通过;4) 一个Java源文件中最多只能有一个public的外部类:因为Java规定     i. 如果.java中一个public的外部类都没有那么该.java文件的名称可以任意取;     ii. 如果.java中有一个public的外部类,那么该.java文件的名称就必须跟该类的名称一样(大小写敏感!);

!!所以一个.java中最多只能有一个public的外部类;

!!可以看到Java的语法规则相当严谨,保证了程序的健壮性;

  1. JavaBean:

    1) JavaBean是一种规范,它规定了一种良好封装的规范,只要符合该规范的类都称作JavaBean;

    2) JavaBean规范:

     i. 每个数据成员(对象成员)都必须是private的; ii. 为每个对象数据成员都提供public的setter和getter; iii. setter和getter的原型必须为:      a. public void set成员名(成员类型 arg);      b. public 成员类型 get成员名();

!例如,成员private String name;的getter和setter应该是:

*1. public void setName(String name);*2. public String getName();

!!getter和setter主要是为了保证对象数据的安全性,防止错误地修改(可以在setter中加入控制逻辑防止错误修改);

  1. 选择何种访问控制符的基本原则:

    1) public:希望暴露给外界自由使用的接口式方法使用public修饰;

    2) private:一般数据成员都应该定义成private,防止外部自由访问,只能通过成员方法操作它;

    3) protected:就是为了继承而使用的,希望子类覆盖的方法用protected修饰;

    4) default:用于包中的各类间协同工作,但又不希望包外使用它;

  2. 包——package:

    1) 为了解决命名冲突,和C++使用命名空间不一样的是,Java使用包来管理类库;

    2) 只要在.java源文件的第一个非注释行放置代码:package 包名;

!!这就把该源文件下的所有类都归到该包中了,其中package是关键字,专门用于定义包;

!!package语句一个源文件中最多只能有一句;

3) 包名的命名规范:     i. 应该全部是小写字母;     ii. 用多个有意义的单词用.连缀而成,中间不要有其它分隔符;     iii. 例如:package com.lirx;    // 该包就是com.lirx4) 完整的类名:     i. 一个类的完整类名应该是包括包路径在内的;     ii. 例如:类Person位于包com.lirx下,那么Person的完整类名应该是com.lirx.Person;     iii. 在任何地方都可以使用类的完整类名(包路径)来表示该类;     iv. 一个类所在的包就相当于该类的命名空间了,就是用来防止命名冲突的;5) javac自动产生包目录结构:     i. 使用javac -d dir Xxx.java后会直接在dir目录下产生和package定义的包路径一样的文件结构;-d指定编译生成的字节码应该放在哪个目录下!     ii. 例如上面的com.lirx.Person的源文件使用javac -d . Person.java编译后,会在当前目录下生成一个/com/lirx/Person.class的目录结构;     iii. Java规定:位于包中的类,在文件系统中也必须与包层次结构相同的目录结构!          a. 原因很简单,因为Java程序就是一个个类,一般一个源文件就代表一个类,在程序中需要用包路径来防止命名冲突,那么在文件系统中同样为了防止命名冲突,使用目录结构就再合适不过了,因此两者统一起来了;          b. 并且Java还规定,如果.class在文件系统中的目录结构和package定义的不符,那就无法在程序中使用这个类!!使用时运行时无法通过,表示找不到该类,也就是说文件目录结构必须和package定义的包路径完全相符才能正常使用程序!!!!          c. 其实javac的-d选项除了可以指定生成项目所在的目录之外还有另一个功能,就是自动根据package语句生成相应的包目录结构!如果没有-d选项则不会生成宝目录结构,这样的类将无法使用,除非自己手动检录目录来符合package定义的包目录结构才行!

!!因此一个良好的习惯就是使用-d选项来编译Java源文件!

      iv. 注意package的包路径和文件系统的包目录结构之间的关系:           a. 是现有package定义的包路径才有包目录结构的!!           b. 即包目录结构必须符合包路径才行!!           c. 并不是你先建立包目录结构包路径才生效的!加入你先建立了一个目录结构:com/lirx/Person.class,而编译时Person.java中并没有package语句,那么Person的完整类名就是Person而不是com.lirx.Person,包路径始终是由package语句定义的!6) 使用Java运行带有包路径的类:     i. 必须在包路径的根目录下使用命令"java 包路径名"才行;     ii. 例如:包路径(目录结构)为dir/com/lirx/Person.class,其包路径名为com.lirx.Person,那么就必须在dir目录下使用命令java com/lirx/Person才行!!     iii. 不能在其它目录下运行该命令,也不允许直接java Person,必须要有完整的包路径!
  1. 用包来管理项目:

    1) 包除了可以防止命名冲突外还可以进行项目管理;

    2) 由于包本身即是一种树状结构(目录结构),因此具有天然地项目管理的优势;

    3) 包的标准的命名规范:公司域名的倒写.项目名.模块名.组件名

!!由于软件公司众多,即是包路径很长也很难保证命名不会冲突,但是网站域名绝对是唯一的,因此用域名的倒写能保证命名绝对不会发生任何冲突!!!

4) 例如一个完整的包路径类名:com.lirx.ebay.submit.hint.Toolbar

!!其中com.lirx是公司域名的倒写,ebay表示一个电子购物网站的项目,submit表示网站的提交模块,hint表示提交模块中的一个自动提示组件(也是一个包),而Toolbar.class就是组件中的一个类,表示工具栏这个类;

  1. 在同一个包中可以省略包路径,在不同包中必须使用完整的包路径:

    1) 包路径一般都很长,每次使用类名都写完整的包路径将会是一件非常麻烦的事,而且代码显得非常臃肿;

    2) Java允许在同一个包中使用包中类可以省略包路径而直接使用类名!!

    3) 但是要注意的是!这个规则不包括具有父子关系的包结构!

     i. 例如包A包含包B,那么在包A的某个类中使用包B的某个类还是必须的用完整的路径名而不能省略包路径!! ii. 因为虽然目录结构上具有父子关系,但它们仍然是两个不同的包!! iii. 因此,只要是不同的包就必须用完整的包路径!!!
  2. 使用import导入包:

    1) 包内的类使用时可以省略包路径,而包外的类想省略包路径就必须使用import语句来导入包了;

    2) import语句必须位于package语句之后和类定义之前!

    3) 导入语法:

     i. 导入单个类:import 类的完整包路径;   // 例如import java.util.Date; ii. 导入包中的全部类:import 包名.*;  // *表示通配符,表示包下的所有类

!!注意!只能表示所有类,但并不能代表子包!!并不能同时导入子包中的所有类!!

4) 使用被导入的类时可以省略包路径,直接使用类名就行了;5) 极端情况:例如java.util包中有个Date,而java.sql包中也有个Date,现在这两个类都要使用,即使import了也无法省略包路径了,因为一个Date是无法判断出使用的是哪个包下的,因此在这种情况下就不能省略包路径了;6) 静态导入:     i. 在JDK 1.5后允许导入类的静态成员和方法了,语法是:import static 导入内容;     ii. 不带static的import只能导入类的对象部分,不能导入静态成员,如果想使用类的静态成员时省略包路径就必须使用import static导入;     iii. 导入语法:          a. 导入某一个具体的静态成员:import static 包路径.成员名;   // 成员可以是静态变量也可以是静态方法(方法就只要写个方法名就行了)

!!例如:import static java.utils.sort; // 就只导入了utils的静态方法sort

          b. 导入一个类的所有静态成员(同时包括变量和方法):import static 包路径.*;

!!同样*表示一个包中的所有静态成员,并不能代表其子包!!

      iv. 典型应用:import static System.*;   // 然后既可以直接在程序中写out.println(..)之类的了

作者:Lirx_Tech 发表于2016/6/23 13:29:35 原文链接
阅读:224 评论:0 查看评论
[译][疯狂Java]面向对象:命名规范、重载、值传递、可变参数、static/this
1. 命名规范:

1) 类:由多个单词连缀而成,单词之间不要分隔符,每个单词首字母大写,其余小写;2) 数据成员:应该是名词,多个单词连缀而成,首字母小写,其余单词首字母大写,其余小写,中间不要使用分隔符;3) 方法:应该以英文动词开头,命名规则和数据成员相同;
  1. 方法重载:

    1) 重载在所有语言中的最基本要求是一致的,必须、至少要求方法名要相同才会有重载的可能;

    2) 重载的条件——方法签名:

     i. 不同语言的方法签名不太相同,像Swift把返回值类型也加入到了方法签名中了,而有些语言像Java就不是; ii. Java规定的方法签名:就只有参数列表(参数个数、类型,但是不管形参名,形参名忽略),只有参数列表不同才会形成重载(不包括返回类型和方法修饰)!

    3) 为什么不加入返回值类型呢?Java是这样解释的,如果参数列表相同但返回值类型不同的方法,Java是允许调用方法不利用返回值的(即单独调用方法,例如对于int f();可以int a = f(); 也可以f();这样调用),即如果出现f()这样的调用方式编译器就无法判断应该使用哪个重载版本,即发生歧义,因此会编译报错!所以Java不把返回值类型加入方法签名中;

  2. 方法的参数传递机制:值传递

    1) Java的方法参数传递全部采用值传递;

    2) 基本类型直接创建器副本作为参数,对于引用类型(对象,只能用引用来访问它)也是值传递,参数仅仅就是指针的副本;

    3) 因此swap方法只对基本类型有效,但是对引用类型无效,因为在内部交换的仅仅是两个副本引用,并不影响外部真正的实参;

  3. 可变参数方法:

    1) 即printf那种参数个数不确定(可变)的方法;

    2) 定义语法:

     i. 可变参数的类型必须写成type...; ii. 例如:public void func(int a, String... args);

!! 传参的时候可变参数可以接受0个或多个实参!!

3) 规则:和其它语言中的可变参数的规定还是基本一致的     i. 可变参数必须是最后一个参数;     ii. 可变参数最多只能有一个;

!!违反这两个规定都会导致调用时产生歧义,很容想明白;

4) 可变参数底层使用数组实现的!!     i. 上面的public void func(int a, String... args);可以这样调用:obj.func(5, new String[]{"abc", "def", "xyz"});     ii. 因为可变参数实质上使用数组实现的,但是这里不能传入多个数组(只能传一个):obj.func(5, new String[]{"abc"}, new String[]{"xyz"});  // 错误!!!5) 尽量不要重载可变参数方法!!     i. 原因是这样的,例子:test(int arg);  和  test(int... args);     ii. 歧义发生在这样调用:test(5),这样会调用test(int arg),但由于两种方法执行内容可能相差很大,因此如果你想在只有一个参数时强制使用test(int... args)版本时就必须使用数组调用,test(new int[]{5});,但这样很麻烦;     iii. 因此为了避免上述这种歧义的情况就尽量不要重载可变参数方法!!
  1. static和this:

    1) 用static修饰的数据成员和方法都属于类,即静态成员,而没有static修饰的成员都属于对象;

    2) this:

     i. 是一个关键字,和C++的this指针一样,Java的this是引用; ii. 用来表示当前的对象本身的引用; iii. 在对象内部(方法、块)中可以通过this来引用自己,因此可以在方法中返回this等,例如:public MyType func() { return this; } iv. 通过this可以轻松地引用(在块或者方法中引用其他成员)对象自己的成员(数据、方法);

    3) this的另一个常见的用途:当方法形参和数据成员重名时可以用this明确指定身份,例如构造器中常见,Person(String name, int age) { this.name = name; this.age = age; }

    4) 对象方法和类方法的本质区别:

     i. 对象方法有一个隐藏参数this(是方法的第一个参数,但不过是隐藏的,用户看不见,底层作为方法的第一个参数传入),因此对象方法可以通过this来识别现在调用的是哪个对象的方法(对象只保存数据成员,所有对象都共享一份方法代码,而方法就是通过this指针来识别调用者的); ii. 而static修饰的类方法就没有this参数,因此静态方法不能访问非静态成员,但是对象方法就能访问静态成员;

    5) 省略this访问其他对象成员:

     i. Java语言本身规定,访问任何对象数据或者方法都必须加上对象前缀,即“obj.data、obj.func(...)"的形式; ii. 因此理论上在一个方法中调用另一个对象方法或者访问一个对象数据成员时应该都加上this前缀,即"this.func()"的形式; iii. 但这样大量地使用this会显得代码很臃肿,因此Java允许在对象本身的范围内引用对象的其它成员可以不加this前缀; iv. 所有没有对象前缀的成员引用编译器都会默认成使用this作为前缀!!因此可以省略this前缀;

    6) 为什么Java允许通过对象引用来访问类成员?!

     i. 讲道理的话在任何地方(不管是类内(外),还是对象内(外))访问类成员(静态成员,数据或方法)都应该使用类名作为前缀,即"MyType.data、MyType.func()"的形式; ii. 但Java也允许用对象应用来访问静态成员,即"obj.static_data、obj.static_func()",这显然非常不合理,容易造成误解!把静态成员错认为是对象成员! iii. 其实这也是由苦衷的!因为Java允许在类(对象)内部访问其他对象成员(静态、非静态)都可以省略前缀,而这些省略都将默认为是通过this引用的!因此那些省略前缀的静态成员也默认使用this作为前缀的,而为了准确地识别它们是静态成员,也就允许通过this来引用静态成员了,而this刚好是对象引用!

!也就是说这种别扭的漏洞是因为this的省略而引起的!!

 7) 总结:也就是说,如果用对象引用来访问静态成员那其实背后也是用类名来代理的,这就会有一个小问题,那就是用空的引用来放问静态成员也是能正常访问的!!

MyType a = null;
a.static_member;
!因为其背后还是用类名来代理的;
!!像这样的代码就非常误导人,因此一定要养成用类名来访问静态成员的好习惯!!

作者:Lirx_Tech 发表于2016/6/23 11:37:26 原文链接
阅读:146 评论:0 查看评论
[译][疯狂Java]正则表达式:捕获组、反向引用、捕获组命名
1. 捕获组及其编号:

1) 捕获组之前讲过,就是匹配到的内容,按照()子表达式划分成若干组;2) 例如正则表达式:(ab)(cd(ef))就有三个捕获组,没出现一对()就是一个捕获组3) 捕获组编号规则:     i. 引擎会对捕获组进行编号,编号规则是左括号(从左到右出现的顺序,从1开始编号;     ii. 例如:
  1. 反向引用:

    1) 捕获组的作用就是为了可以在正则表达式内部或者外部(Java方法)引用它;

    2) 如何引用?当然是通过前面讲的用捕获组的编号来引用咯!

    3) 正则表达式内部引用:

     i. \X:X是一个十进制数,X的范围必须落在捕获组编号范围之内,该表达式就匹配X号捕获组所匹配到的内容; ii. 从上面的描述可以看出,\X匹配的内容是必须X号捕获组匹配成功之后才能确定的! iii. 例如:([ab])\1,匹配aabbcc的结果是aa和bb,\1的内容必须要让1号捕获组捕获后才能确定,如果1号捕获的是a那么\1就是a,1号捕获到了b那么\1就是b;

    4) 正则表达式外部引用:就是用Matcher对象的start、end、group查询匹配信息时,使用捕获组编号对捕获组引用(int group);

  2. 捕获组命名:

    1) 如果捕获组的数量非常多,那都用数字进行编号并引用将会非常混乱,并且难以记忆每个捕获组的内容及意义,因此对捕获组命名显得尤为重要;

    2) Java 7开始提供了对捕获组命名的语法,并且可以通过捕获组的名称对捕获组反向引用(内外都行);

     i. 命名捕获组的语法格式:(?<自定义名>expr) ii. 例如:(?<year>\d{4})-(?<date>\d{2}-(?<day>\d{2}))     a. 有三个命名捕获组year、date和day     b. 从左到右编号分别为1、2、3(编号同样是有效的)

    3) 命名捕获组的反向引用:

    i. 正则表达式内引用:\k<捕获组名称>

!例如:(?\d{4})-\k可以匹配1999-1999

    ii. 外部引用:Matcher对象的start、end、group的String name参数指定要查询的捕获组的名称;
  1. 普通捕获组和命名捕获组的混合编号:

    1) 普通捕获组是相对命名捕获组的,即没有显式命名的捕获组;

    2) 当所有捕获组都是命名捕获组时那么编号规则和原来相同,即按照左括号(的出现顺序来编号;

    3) 当普通捕获组和命名捕获组同时出现时,编号规则为:先不忽略命名捕获组,只对普通捕获组按照左括号顺序编号,然后再对命名捕获组从左往右累计编号,例如:

!先忽略命名命名捕获组,先对普通捕获组编号\d{4}是1,\d\d是2,然后再接着累加地对命名捕获组编号,因此是3;

作者:Lirx_Tech 发表于2016/6/20 16:12:47 原文链接
阅读:621 评论:0 查看评论
[译][疯狂Java]正则表达式:Java正则表达式语法、贪婪模式/勉强模式
1. 基本字符和特殊字符:

1) 正则表达式前面讲过了,是需要先编译再执行的,因此从某种方面来将正则表达式其实也是一种特殊的编程语言;2) 既然是编程语言(特殊的编程与语言)那么它就有它自己的源代码字符、关键字等概念了;3) 正则表达式没有像其它编程语言一样的关键字(像if、else、while等),它的关键字是特殊字符,因此正则表达式的源码分为基本字符和特殊字符:4) 基本字符:包括所有的英文字母(大小写都包含)和数字(0-9),它们就是纯文本字符,它们只代表自己本身;5) 特殊字符:基本上都是一些符号字符,例如*、\、^等,它们不是纯文本字符,有特殊含义,正则表达式引擎会把它们解释成命令并编译成子函数;6) 这里先介绍两个最重要的特殊字符,它们在后面会有大量应用:     i. \:转义字符,用于转义紧跟在它后面的下一个字符,使其具有特殊含义;     ii. ():标记子表达式的开始和结束,即用()括起来的部分就是一个子表达式;
  1. 匹配单个字符:

    1) 基本字符(普通字符):所有的英文字母(包括大小写)和纯数字(0-9),之前已经讲过了;

    2) 用ASCII编码来表示字符:有两种形式,一种是8进制形式,一种是16进制形式

     i. \0XX:X表示八进制数(0-7); ii. \xXX:X表示十六进制数(0-9、a-f,不分大小写,,一般推荐大写,大写更美观);

!!由于这两种形式只能表示ASCII编码,而ASCII编码范围是十进制的[0, 127],因此八进制表示法的X最多有2个,而十六进制的X最多也只有两个!!

3) 用Unicode编码来表示字符:\uXXXX

!!X表示十六进制数(字母不区分大小写),必须是4个X,不能缺省(ASCII码表示法可以缺省);

4) 控制符:     i. \t:制表     ii. \n:换行     iii. \r:回车     iv. \a:报警     v. \e:Escape     vi. \cX:Ctrl+X,例如\cM就表示Ctrl+M,其中X是英文字母(包括大小写)
  1. 中括号表达式:范围匹配,一种特殊的匹配单个字符的方法

    1) 即[ ]表达式,用来匹配单个字符,只不过这一个字符的所在范围有中括号内的表达式来确定

    2) 枚举:[abc],表示匹配a、b、c的任意一个

    3) 范围:[a-f],表示匹配a-f之间的任意一个字符,包括边界,其中右边界一定要≥左边界,否则引擎编译错误

    4) 并:[a-cm-p],就表示a-c的范围和m-p的范围求并,其实前面的abc之类的枚举也是一种并运算

    5) 交:[a-z&&b-d],就表示a-z的范围和b-d的范围求交,结果等于[b-d]

    6) 补:[^abc]表示非a、b、c的任意一个字符,[^a-f]表示非a-f的任一字符;^也是一个脱字符,必须是中括号表达式的第一个字符,否则不起任何作用!!

!补的是^后面紧跟的整个表达式!

7) 嵌套:[a-m&&[def]],a-m和[def]都表示范围,因此可以做运算,结果等于[d-f]8) 有了上面这些基本运算,就可以构造一些很复杂的运算了:[a-z&&[^bc]] == [ad-z]

!!但是注意不要用^构造复杂表达式,以下一些表达式将不起作用!

    a. ^后面紧跟一个[ ]嵌套:^[a-d],非法!不起任何作用    b. ^后面是一个运算(交并补):^a-cf-h、^a-z&&c-h、^^a-c,都是非法的!!不起任何作用!!    c. ^后面只能跟单纯的枚举和范围运算!!例如[^azh]、[^a-h]等;
  1. 前面紧邻的子表达式的重复次数:

    1) *:重复0次或多次

    2) +:重复1次或多次

!!常用:\w+表示匹配一个单词!

3) ?:重复0次或1次4) {n}:刚好重复n次    {n,}:重复≥n次,即≥n次的都可以    {n,m}:重复[n, m]次,即n≤  ≤m的都可以

!!不能有任何多余的空格,否则直接编译(引擎编译)不通过;

!!n必须为非负整数,且n≤m,否则也会引擎编译不通过;

!!贪婪模式和勉强模式:解决重复次数匹配的歧义(就是上面的重复次数匹配)

    a. 由于重复次数的匹配可能匹配到多种结果,例如:x{2, 3}匹配xxxx,既可以匹配xx,也可以匹配xxx,因此存在歧义;    b. 但是任何编程语言,包括正则表达式,都是不允许有任何歧义的,因此底层必定有避免歧义的机制;    c. 正则表达式引擎默认使用贪婪模式解决歧义问题,贪婪模式:只要符合要求就一直匹配下去,直到无法匹配为止,即上面的结果就是2和3都存在,就往大的取!只取上限,包括*、+、?都是一样的,例如ab.*zz匹配abcxxzzsfewzzq的结果是abcxxzzsfewzz,并没有在abcxxzz处停止,而是直到最后的zzq不能再匹配时才停止匹配!    d. 与贪婪模式对应的就是勉强模式:只匹配下限,即只往少的匹配,一找到符合要求的就立马匹配成功,不再管后面的序列,在勉强模式下*匹配0个,+匹配一个,?匹配0个,而{ }只匹配下限!    f. Java是支持勉强模式的,勉强模式的表示方式是重复次数字符后面紧跟一个?,例如??就表示勉强模式的?,+?就表示勉强模式的+,*?就表示勉强模式的*,{ }?就表示勉强模式的{ };
  1. 通配符:

    1) .:匹配\r和\n之外的任意其他字符

    2) \d:匹配任意一个数字(0-9),即digit

    3) \D:匹配任意一个非数字

    4) \s:匹配任意一个空白符(制表、换行、空格、换页、回车等),即space

    5) \S:匹配任意一个非空白符

    6) \w:匹配任意一个单词字符,单词字符指0-9、26个英文字母(包括大小写)、下划线(_),注意是一个字符!而不是一个单词

    7) \W:匹配任意一个非单词字符

  2. 匹配起始和结尾:锚

    1) ^:脱字符,匹配主串的开头,即必须以^后面紧跟的子表达式为开头,例如^abc可以匹配abcxxx,group的返回结果是abc

    2) 前面紧跟的子表达式结尾,例如abc$可以匹配qqqxxabc,group的返回结果是abc

    3) 组合锚点:例如,^book.*end$可以匹配所有以book起始以end结尾的子串

    4) 脱字符必须作为模式串的起始,锚字符必须作为$模式串的结束,否则将不会起到任何作用!!

!例如,abc^xq并不能匹配abcxquuu,而abc$zx并不能匹配abczxddd!!

  1. 匹配单词边界:boundary

    1) \b:前面紧邻的子表达式必须是单词的边界(单词之间用空白符分隔),例如:ok\b可以匹配”book lala”中的book的ok,但不能匹配”books lala”,group返回的也是ok

    2) \B:前面紧邻的子表达式必须“不是”单词的边界,例如:ok\B可以匹配”books lala”中的books的ok,但不能匹配”book lala”,group返回的也是ok

  2. 或运算:|

    1) 表示指定两项(表达式)之间任选一项,即或运算;

    2) 例如a|b等于[ab],(public)|(private)就表示这两个单词任意匹配一个,但最好不要写成public|private,这可能表示匹配publicrivate或者publiprivate,有些引擎就会判定歧义;

!!良好的习惯:自己不确定是否产生歧义的时候就加一下()变成子表达式,一目了然,而且可以避免错误!!

  1. 所有特殊字符的纯文本形式都必须转义!

    1) 像.、^、$、\、|、[、]、{、}、(、)等特殊字符,如果想表示其纯文本形式,都必须使用\转义;

    2) 例如(就表示纯文本的’(‘字符;

  2. 在Java源代码中编写正则表达式:

    1) 由于正则表达式使用的是自己的字符,而Java源代码也有自己的字符,这就会导致两者之间发生一些不可避免的冲突;

    2) 最大的冲突就是\,正则表达式中\就是一个字符(特殊字符),而Java中要表示纯文本的\也需要转义,即用\来表示纯文本的\;

    3) 而Java源代码中编写的正则表达式对于Java源代码来说应该是纯文本,这就意味着,Java源代码中编写的正则表达式中的\都必须要用\来表示;

    4) 例如:正则表达式\w在Java源码中编写就必须写成\w,比较麻烦

!小结:所有在Java源码中编写的正则表达式中的\都必须用\表示,如果你觉得上面的原理看着头晕,那就记住这句简单的小结即可,无脑使用;

!!即先正常写好正则表达式,然后把里面所有的\换成\即可;

5) 一般为了避免这种麻烦的事情,都是先在程序外部的配置文件中写好正则表达式,然后在程序中读取、编译并使用,这就避免了两者字符集的冲突了;

作者:Lirx_Tech 发表于2016/6/20 15:10:31 原文链接
阅读:216 评论:0 查看评论
[译][疯狂Java]正则表达式:Pattern、Matcher、String对正则表达式的支持
1. 正则表达式引擎:

1) 正则表达式其实跟SQL语句一样,其实本质都是查询命令,需要先编译然后再用相应的执行引擎启动并查询;2) 为什么正则表达式的本质是命令?     i. 由于正则表达式中包含很多特殊字符,如*、^等,它们并不属于普通的纯文本字符;     ii. 它们对于引擎来讲其实是一种特殊的命令(动作),例如*会被解释为“连续0个或多个任意字符”;     iii. 那么引擎就会把*编译成一条命令(一个函数,底层是flex/bison实现的引擎),该命令的内容是“匹配连续0个或多个任意字符"     iv. 也就是说正则表达式在编译后会被拆分成(按照子表达式)若干个函数,匹配时会组合这些函数进行匹配! 3) 因此正则表达式跟SQL语句一样,必须要先经过编译才能执行;
  1. Pattern:正则表达式

    1) 该类就表示正则表达式,该类的对象表示一个编译好的正则表达式;

!!其实正则表达式也叫模式串,那它去匹配的串叫做主串,因此正则表达式也可叫做pattern(即模式的意思);

2) 但是编译是需要手动显式完成的,要利用Pattern的静态工具方法compile来完成:static Pattern Pattern.compile(String regex);

!!Pattern不提供构造器,正则表达式只能通过编译获得!

3) 编译的另一个好处:其实跟SQL的preparedStatement一样,编译过了以后可以重复使用(已经保存在内存中)无需再编译,加到了执行效率;4) Pattern和String一样,是不可变类,即编译后该对象的内容就确定了,无法修改,正因为这种只读的性质,Pattern必然是线程安全的,并发使用没有安全隐患;5) 获取Pattern本身的表达式字符串:编译时会把表达式字符串本身regex保存在Pattern对象的内部数据成员pattern中     i. public String pattern() { return pattern; }     ii. public String toString() { return pattern; }

!!例如:Pattern p = Pattern.compile(“\w+”); 那么p.pattern()得到的就是”\w+”这个模式串本身

!!其中Pattern重载了toString,因此可以很方便的用System的print系列方法直接打印Pattern对象,得到的将是正则表达式本身的字符串;

  1. Matcher:正则表达式引擎

    1) 有了Pattern对象以后就可以用Matcher来匹配正则表达式了,其实从Matcher这个名字也可以看出,该类对象就是用来执行匹配动作的,它就是正则表达式引擎;

    2) Matcher包含两个要素,第一就是模式串(正则表达式Pattern),第二就是主串了(就是用模式串去匹配的串):

     i. 也就是说Matcher里包含一个Pattern对象(成员)表示模式串(parentPattern),也必须要包含一个CharSequence对象(成员)表示主串(text); ii. 接着调用Matcher的各种对象方法就可以完成匹配、查找等一系列操作了; iii. 很可惜Matcher虽然有这样的构造器(Matcher(Pattern parent, CharSequence text);)但是该构造器不可见,不能在外部使用;

    3) 必须使用Pattern对象的matcher方法构造Matcher对象:Matcher Pattern.matcher(CharSequence input);

!!该方法将构造一个Matcher对象并返回,以当前的Pattern对象作为parentPattern成员(模式串),以input作为text成员(主串);

!!例如:Pattern p = Pattern.complie(“\w+”); Matcher m = p.matcher(“hello everyone”); 等于: Matcher m = new Matcher(Pattern.complie(“\w+”), “hello everyone”);

4) Matcher的常用方法:都是对象方法     i. 重置:返回的是重置后的Matcher        a. Matcher reset();  // 由于进行匹配操作的时候需要维护很多状态(比如当前匹配到了什么位置之类的),该操作会清除所有状态,回到开始匹配之前        b. Matcher reset(CharSequence input);  // 清除状态的同时将主串重设为input

!!很可惜,Matcher可以重设主串,但不能重设模式串,模式串是嵌入到Matcher中的;

     ii. 获取Matcher中嵌入的模式串:Pattern pattern();   // 返回嵌入的Pattern对象
  1. 使用Matcher进行匹配操作:都是Matcher的对象方法

    1) 位置指针:由于匹配的过程其实也是扫描主串的过程,因此必须要维护一个位置指针来指示当前扫描到了什么位置;

向下匹配:

2) boolean find();     i. 从当前位置向下继续匹配,如果匹配到了一个子串就返回true,如果一直扫描到主串末尾还没有符合要求的子串就返回false;     ii. 一旦匹配成功就会保存状态,例如保存当前匹配成功的子串;3) boolean find(int start);     i. 从主串的start位置处开始匹配;     ii. 该方法会重设位置指针,这也就意味着要重新开启一轮新的匹配;     iii. 因此该方法会默认清楚所有状态,就相当于先reset,然后再从主串的start位置处进行boolean find()操作;

查询匹配到的各种信息:是和find系列配合使用的,一旦find成功,就可以调用下面的方法查询此次匹配所保存的各种信息

4) int start();  // 返回本次匹配到的子串在主串中的起始位置5) int end();  // 返回本次匹配到的子串在主串中的结束位置(最后一个字符的索引+1)>>>获得捕获组:

!!这里只是先略微简介一下捕获组的概念:

     a. 起始捕获组就是匹配到的子串,即本次匹配捕获到的匹配内容,这就是捕获的含义,捕获就是匹配的意思;     b. 正则表达式引擎会将捕获到的(匹配成功的)子串分割成若干组;     c. 分组的依据就是正则表达式中的括号位置,例如表达式"(a)(b)(c)",该表达式将匹配字符串"abc",那么匹配到的结果会被分成三组(按照括号分),分别是"a"、"b"、"c",会对这些组一次编号,从1开始计,"a"就是组1,"b"是组2,"c"是组3;6) String group();  // 返回所有组的连接,即返回整个匹配到的子串7) String group(int group);   // 返回第group组的内容8) String group(String name);  // 返回名字为name的组的内容

!!Java 7以后支持在正则表达式中对捕获组命名(毕竟用组号来引用捕获组不见名知意,不方便编程,如果捕获组数量庞大,那么数字引用的方式将会非常不友好);

!!具体的命名规则以及正则表达式的语法会在下一章中详细讲解

!!有了捕获组的概念后,start和end也可以查询相应的捕获组的位置信息了:

    a. int start([int group | String name]);    b. int end([int group | String name]);

!!分别返回指定捕获组的位置信息,如果无参就是返回整个匹配到的子串的位置信息;

9) 标准的配合find进行匹配的示例:

while (m.find()) {
String s = m.group(); // m.start()、m.end()等

}

全局匹配:
10) boolean matches(); // 必须整个主串匹配才返回true

11) boolean lookingAt();  // 只要主串的开头匹配就可以返回true,比如用"abc"匹配主串"abcxxx"就行,因为主串前缀能匹配上"abc"
  1. String对正则表达式的支持:

    1) 除了基本的正则表达式功能之外,String也提供了正则表达式匹配的支持;

    2) 以下介绍的都是String的对象方法,用于正则表达式相关的操作;

    3) 检查String对象是否完全匹配一个正则表达式:boolean matches(String regex); // 必须是全局的完全匹配,并不是只要包含就行!

    4) 全局替换:String replaceAll(String regex, String replacement); // 将所有匹配regex的子串都替换成replacement字符串

    5) 只替换第一个匹配到的子串:String replaceFirst(String regex, String replacement);

    6) 切分:String[] split(String regex); // 以regex作为分隔符将字符串分割成多个子串

作者:Lirx_Tech 发表于2016/6/14 16:47:21 原文链接
阅读:300 评论:0 查看评论
[译][疯狂Java]泛型:泛型的底层原理(类型擦除、原生类型、编译前检查)
1.
作者:Lirx_Tech 发表于2016/6/7 21:07:43 原文链接
阅读:184 评论:0 查看评论
[译][疯狂Java]泛型:泛型构造器、泛型方法的重载问题、泛型数组的问题(应该摒弃)、?下限的典型应用
1. 泛型构造器:

1) 构造器也是方法,既然有泛型方法,那么构造器也可以定义为泛型方法,那么就变成了泛型构造器;2) 由于构造器也是方法,因此反省构造器的定义和普通泛型方法的定义完全相同,例如:public <T> MyClass(T t) { ... }3) 使用泛型构造器:      i. 和使用普通泛型方法一样没区别,一种是显式指定泛型参数,另一种是隐式推断,如果是显式指定则以显式指定的类型参数为准,如果传入的参数的类型和指定的类型实参不符,将会编译报错;

!典型示例:A a = new A(“lala”); // 菱形实参还是写在方法名之前

      ii. 这里唯一需要特殊注明的就是,如果构造器是泛型构造器,同时该类也是一个泛型类的情况下应该如何使用泛型构造器:          a. 因为泛型构造器可以显式指定自己的类型参数(需要用到菱形,放在构造器之前),而泛型类自己的类型实参也需要指定(菱形放在构造器之后),这就同时出现了两个菱形了,这就会有一些小问题,具体用法再这里总结一下;          b. 这里使用的类是这样定义的:public A<T> { public <E> A(E e) { ... } }          b. 全指定,例如:A<String> a = new <Integer>A<String>(15); // 所有类型实参全部显式指定,Integer代表E,String代表T          c. 全隐藏,例如:A<String> a = new A<>(15);   // 可以隐式推断出Integer是E,String是T          d. 半隐藏,例如:A<String> a = new A<String>(15);  // 还是可以推断出E是Integer,而String是T          e. 上面的叫做半隐藏T,但是不能半隐藏E,例如:A<String> a = new <Integer>A<>(15);  // 虽然也可以正确推断,但是这种语法不允许!!会直接编译报错!

!!因此这里麻烦的是需要稍微记忆一下,不能半隐藏E

!!平时使用的时候就使用其中一种,推荐是全隐藏,越简洁越好,就是用一种就不会增加记忆负担;

  1. 泛型方法的重载问题:

    1) 泛型方法的定义非常灵活,只要形式不同就能形成重载,例如List、List、List

0 0
原创粉丝点击