本文回顾Java编程语言的要点。以下内容如无特殊说明都是在Java语境下。


逻辑运算和位运算

逻辑运算符主要用来操作布尔值。Java中的逻辑运算符包括,&与, 或,&&短路与,   短路或,!非,^异或
运算符 说明
& [与] 两个操作数为true,结果才是true,否则是false
| [或] 两个操作数有一个是true,结果就是true
&& [与] 只要有一个为false,则直接返回false
|| [或] 只要有一个为true, 则直接返回true
! [非] 取反: !false为true,!true为false
^ [异或] 相同为false,不同为true

一般都使用短路与或者短路或,可以提高程序执行效率。

位运算是二进制位的运算。其中位移运算相当于乘2或者除2,但是计算速度相对于正常的乘除运算要快非常多。

运算符 说明
~ 取反
& 按位与
| 按位或
^ 按位异或
<< 左移运算符,左移1位相当于乘2
>> 右移运算符,右移1位相当于除2取商
int a = 3*2*2;
int b = 3<<2; //相当于:3*2*2;
int c = 12/2/2;
int d = 12>>2; //相当于12/2/2;

面向对象内存分析

Java虚拟机的内存分为三个区域:栈stack,堆heap,和方法区method area。

其实,方法区也在堆里面,所以本质上就是两个地方,栈和堆。

栈是表示方法执行的内存模型,也就是说函数执行,方法调用等等过程都体现在栈中。每个方法被调用都会创建一个栈帧用来储存局部变量,操作数和方法出口等。一个Java程序可能包括多个线程,JVM为每个线程开辟一个栈,栈属于线程私有,不能共享。栈由系统自动分配,速度快,是一个连续的内存空间。

堆用来储存创建好的对象和数组[数组也是对象],JVM只有一个堆空间,所有线程共享该堆空间。堆是一个不连续的内存空间,分配灵活但是速度慢。

方法区也叫静态区,也在堆空间中,被所有线程共享,主要用于存储类代码信息,静态变量和方法,以及字符串常量,JVM也只有一个方法区。

比如有一个test.java,要运行这个类首先通过javac test.java来编译,然后通过java test来运行。java test命令就会将test类的相关信息,静态变量和字符存常量加载到方法区。随着程序运行,方法函数会通过先进后出的方式加载到栈区,如果遇到创建对象的操作,就会在堆区存储对应的对象并将内存地址返回给方法。遇到字符串赋值,就把方法区中的字符串常量的地址告诉给对应的变量来完成赋值操作


垃圾回收 [Garbage Collection]

Java的垃圾回收由垃圾回收器自动完成,不需要开发人员参与。主要的垃圾回收算法有引用计数法和引用可达法

引用计数法–堆中每个对象都有一个引用计数。被引用一次,计数加1。被引用变量值变为null,则计数减1,直到计数为0,则表示变成无用对象。优点是算法简单,缺点是循环引用的无用对象无法别识别。

引用可达法–程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

分代回收机制

我们将对象分为三种状态:年轻代、年老代、持久代。JVM将堆内存划分为Eden、Survivor和Tenured/Old空间。

年轻代–所有新生成的对象首先都是放在Eden区。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次Minor GC会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当年轻代区域存放满对象后,就将对象存放到年老代区域。

Minor GC–用于清理年轻代区域。Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中(这两个区,大小空间也相同,同一时刻Survivor1和Survivor2只有一个在用,一个为空)

年老代–在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。

Major GC–用于清理老年代区域。

Full GC–用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。

持久代–用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。


this 关键字

this关键字的本质就是创建的对象的地址,所以就是指的当前对象。所以一般来说,this.成员变量this.成员方法就相当于当前类.成员变量当前类.成员方法。所以,在程序中产生二义性之处,应使用this来指明当前对象;普通方法中,this总是指向调用该方法的对象。构造方法中,this总是指向正要初始化的对象。

使用this关键字调用重载的构造方法[可以根据参数列表重载不同的构造方法],避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句

this不能用于static方法中。因为static方法位于方法区只有类相关信息没有对象,使用this会找不到对应的对象。

比如下面的代码

public class TestThis {
    int a, b, c;
 
    TestThis() {
        System.out.println("正要初始化一个Hello对象");
    }
    TestThis(int a, int b) {
        // TestThis(); //这样是无法调用构造方法的!
        this(); // 调用无参的构造方法,并且必须位于第一行!
        a = a;// 这里都是指的局部变量而不是成员变量
        // 这样就区分了成员变量和局部变量. 这种情况占了this使用情况大多数!
        this.a = a;
        this.b = b;
    }
    TestThis(int a, int b, int c) {
        this(a, b); // 调用带参的构造方法,并且必须位于第一行!
        this.c = c;
    }
 
    void sing() {
    }
    void eat() {
        this.sing(); // 调用本类中的sing();
        System.out.println("你妈妈喊你回家吃饭!");
    }
 
    public static void main(String[] args) {
        TestThis hi = new TestThis(2, 3);
        hi.eat();
    }
}

static 关键字

用static修饰的变量叫静态变量,用static修饰的方法叫静态方法。

用static声明的成员变量为 静态成员变量,也称为 类变量。类变量和方法的生命周期和类相同,在整个应用程序执行期间都有效。而普通的成员变量和方法从属于对象。

静态成员变量有如下特点,

  • 为该类的公用变量,属于类,被该类的所有实例共享,在类被载入时被显式初始化。

  • 对于该类的所有对象来说,static成员变量只有一份。被该类的所有对象共享!!

  • 一般用类名.类属性/方法来调用。(也可以通过对象引用或类名(不需要实例化)访问静态成员。)

  • 在static方法中不可直接访问非static的成员。因为static方法无法访问对象的成员变量或者方法。

静态方法

非静态方法有一个和静态方法很重大的不同:非静态方法有一个隐含的传入参数,该参数是JVM给它的,和我们怎么写代码无关,这个隐含的参数就是对象实例在stack中的地址指针。因此非静态方法[在stack中的指令代码]总是可以找到自己的专用数据[在heap中,准确说是静态区的对象属性值]。当然非静态方法也必须获得该隐含参数,因此非静态方法在调用前,必须先new一个对象实例,获得stack中的地址指针,否则JVM将无法将隐含参数传给非静态方法

而静态方法无此隐含参数,因此也不需要new对象,只要class文件被ClassLoader load进入JVM的stack,该静态方法即可被调用。当然此时静态方法是存取不到heap中的对象属性的,因为对象没有被实例化。

除此之外,在加载时机和占用内存上,静态方法和实例方法是一样的,在类型第一次被使用时加载。调用的速度基本上没有差别。

静态初始化块

静态初始化块是一个static修饰的代码块,会在类初始化的时候执行。

public class User3 {
    int id;        //id
    String name;   //账户名
    String pwd;   //密码
    static String company; //公司名称
    static {
        System.out.println("执行类的初始化工作");
        company = "北京尚学堂";
        printCompany();
    }  
    public static void printCompany(){
        System.out.println(company);
    }  
    public static void main(String[] args) {
        User3  u3 = null;
    }
}

在声明初始化User3这个类并声明u3的时候,并没有生成对象,但是这时会执行静态代码块,因为它和类的生命周期相同,初始化类的时候也会加载static代码块。如果有构造函数,static代码块也会先于构造函数执行,因为根据Java内存管理执行,类初始化的时候会在方法区加载类相关信息和静态信息,这个时候还没有构造对象也就还没有执行构造函数


Java参数传递

Java中,方法中所有参数都是值传递,也就是传递的是值的副本。 也就是说,我们得到的是原参数的复印件,而不是原件。因此,复印件改变不会影响原件。

基本数据类型参数的传值:传递的是值的副本。副本改变不会影响原件。

引用类型参数的传值:传递的是值的副本。但是引用类型指的是对象的地址。因此,副本和原参数都指向了同一个地址,改变副本指向地址对象的值,也意味着原参数指向对象的值也发生了改变。


Java Package 导入

Java package导入需要使用import关键字,java.lang是唯一一个不需导入就可直接使用的包。

静态导入[static import]是在JDK1.5新增加的功能,其作用是用于导入指定类的静态属性,这样我们可以直接使用静态属性。

package cn.sxt;
 //以下两种静态导入的方式二选一即可
import static java.lang.Math.*;//导入Math类的所有静态属性
import static java.lang.Math.PI;//导入Math类的PI属性
 
public class Test2{
    public static void main(String [] args){
        System.out.println(PI);
        System.out.println(random());
    }
}

Java 继承 & instanceof

父类也称作超类、基类、派生类等。

Java中只有单继承,没有像C++那样的多继承。多继承会引起混乱,使得继承链过于复杂,系统难于维护。

Java中类没有多继承,接口有多继承。

子类继承父类,可以得到父类的全部属性和方法[除了父类的构造方法],但不见得可以直接访问[比如,父类私有的属性和方法]。

如果定义一个类时,没有调用extends,则它的父类是:java.lang.Object。

instanceof是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。


Overload vs Override

重载[overload] 重载的方法,实际是完全不同的方法,只不过名称相同但是形参类型,形参个数,形参顺序不同[也就是说调用的时候可以区分]。

重写也是覆盖[override] 子类通过重写父类的方法,可以用自身的行为替换父类的行为。方法的重写是实现多态的必要条件。

方法的重写需要符合下面的三个要点:

  • ==: 方法名、形参列表相同。
  • ≤:返回值类型和声明异常类型,子类小于等于父类。
  • ≥: 访问权限,子类大于等于父类。

super 关键字

super是直接父类对象的引用。可以通过super来访问父类中被子类覆盖的方法或属性

使用super调用普通方法,语句没有位置限制,可以在子类中随便调用。

若是构造方法的第一行代码没有显式的调用super(…)或者this(…);那么Java默认都会调用super(),含义是调用父类的无参数构造方法。这里的super()可以省略。


Java 封装

Java封装追求的是高内聚低耦合。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。

  • private 表示私有,只有自己类能访问

  • default表示没有修饰符修饰,只有同一个包的类能访问

  • protected表示可以被同一个包的类以及其他包中的子类访问

  • public表示可以被该项目的所有包中的所有类访问

一般的原则是default和protected很少用到。

一般使用private访问权限来设置类的成员便利。

提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)。

一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰。


Java 多态

多态指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。比如:同样是调用人的休息方法,张三是睡觉,李四是旅游,高淇老师是敲代码,数学教授是做数学题; 同样是调用人吃饭的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。

多态的要点:

  1. 多态是方法的多态,不是属性的多态(多态与属性无关)。

  2. 多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。

  3. 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。

下面的例子说明了多态属性

class Animal {
    public void shout() {
        System.out.println("叫了一声!");
    }
}
class Dog extends Animal {
    public void shout() {
        System.out.println("旺旺旺!");
    }
    public void seeDoor() {
        System.out.println("看门中....");
    }
}
class Cat extends Animal {
    public void shout() {
        System.out.println("喵喵喵喵!");
    }
}
public class TestPolym {
    public static void main(String[] args) {
        Animal a1 = new Cat(); // 向上可以自动转型
        //传的具体是哪一个类就调用哪一个类的方法。大大提高了程序的可扩展性。
        animalCry(a1);
        Animal a2 = new Dog();
        animalCry(a2);//a2为编译类型,Dog对象才是运行时类型。
         
        //编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
        // 否则通不过编译器的检查。
        Dog dog = (Dog)a2;//向下需要强制类型转换
        dog.seeDoor();
    }
 
    // 有了多态,只需要让增加的这个类继承Animal类就可以了。
    static void animalCry(Animal a) {
        a.shout();
    }
 
    /* 如果没有多态,我们这里需要写很多重载的方法。
     * 每增加一种动物,就需要重载一种动物的喊叫方法。非常麻烦。
    static void animalCry(Dog d) {
        d.shout();
    }
    static void animalCry(Cat c) {
        c.shout();
    }*/
}

对象转型 casting

父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。

向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行类型的强制转换,我们称之为向下转型。

在向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类型转换异常ClassCastException


final 关键字

修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值

修饰方法:该方法不可被子类重写。但是可以被重载!

修饰类: 修饰的类不能被继承。比如:Math、String等。


抽象方法和抽象类

抽象方法:使用abstract修饰的方法,没有方法体,只有声明。定义的是一种规范就是告诉子类必须要给抽象方法提供具体的实现

抽象方法的使用场景可以是,一个类经常需要被继承,而其中一个方法属于多态,每个继承的子类几乎都要重写,所以可以将这个方法定义为抽象方法,让子类自行实现。

抽象类:包含抽象方法的类就是抽象类。通过abstract方法定义规范,然后要求子类必须定义具体实现。通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。

抽象类严格定义子类的规范,子类必须重写[实现]抽象方法。抽象类把类的设计和实现分离

抽象类的使用要点:

  1. 有抽象方法的类只能定义成抽象类

  2. 抽象类不能实例化,即不能用new来实例化抽象类。

  3. 抽象类可以包含属性、方法、构造方法。但是构造方法不能用来new实例,只能用来被子类调用

  4. 抽象类只能用来被继承。

  5. 抽象方法必须被子类实现。

如果子类继承抽象父类,并且调用一个抽象父类中的非抽象方法,同时这个非抽象方法中调用了父类的抽象方法,同时这个父类的抽象方法被子类实现,那么子类调用父类的非抽象方法就会这个实现。


Java 接口

接口就是比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。

抽象类还提供某些具体实现,接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批类具有的公共方法规范。

从接口的实现者角度看,接口定义了可以向外部提供的服务。

从接口的调用者角度看,接口定义了实现者能提供那些服务。

接口是两个模块之间通信的标准,通信的规范。如果能把你要设计的模块之间的接口定义好,就相当于完成了系统的设计大纲,剩下的就是添砖加瓦的具体实现了。做系统时往往就是使用“面向接口”的思想来设计系统。

接口和实现类不是父子关系,是实现规则的关系。比如:我定义一个接口Runnable,Car实现它就能在地上跑,Train实现它也能在地上跑,飞机实现它也能在地上跑。就是说,如果它是交通工具,就一定能跑,但是一定要实现Runnable接口。

所以对比来说, 普通类有具体实现,抽象类有具体实现和规范(抽象方法),接口只有规范!

定义接口的详细说明:

  1. 访问修饰符:只能是public或默认。

  2. 接口名:和类名采用相同命名机制。

  3. extends:接口可以多继承。

  4. 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。

  5. 方法:接口中的方法只能是:public abstract。 省略的话,也是public abstract。

要点:

  1. 子类通过implements来实现接口中的规范。

  2. 接口不能创建实例,但是可用于声明引用变量类型。

  3. 一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。

  4. JDK1.7之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。

  5. JDK1.8后,接口中包含普通的静态方法。


Callback & Hook

回调函数[callback]或者钩子函数[Hook],其实也是设计模式的模板方法模式。先设计好一个方法调用,但是这个方法调用依赖于传入的对象类型,不同的对象会产生不同的函数实现。

模板方法模式很常用,其目的是在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。在标准的模板方法模式实现中,主要是使用继承的方式,来让父类在运行期间可以调用到子类的方法。 详见抽象类部分示例。

其实在Java开发中,还有另外一个方法可以实现同样的功能,那就是Java回调技术。回调是一种双向的调用模式,也就是说,被调用的接口被调用时也会调用对方的接口,简单点说明就是:A类中调用B类中的C方法,然后B类中的C方法中反过来调用A类中的D方法,那么D这个方法就叫回调方法。

回调的具体过程如下:

  1. Class A实现接口CallBack —— 背景1

  2. class A中包含class B的引用 ——背景2

  3. class B有一个参数为CallBack的方法C ——背景3

  4. 前三条是我们的准备条件,接下来A的对象调用B的方法C

  5. 然后class B就可以在C方法中调用A的方法D

这样说大家可能还是不太理解,下面我们根据示例5-33来说明回调机制。该示例的生活背景为:有一天小刘遇到一个很难的问题“学习Java选哪家机构呢?”,于是就打电话问小高,小高一时也不太了解行情,就跟小刘说,我现在还有事,等忙完了给你咨询咨询,小刘也不会傻傻的拿着电话去等小高的答案,于是小刘对小高说,先挂电话吧,你知道答案后再打我电话告诉我吧,于是挂了电话。小高先去办自己的事情去了,过了几个小时,小高打电话给小刘,告诉他答案是“学Java当然去北京尚学堂”。

【示例5-33】回调机制示例

/** 
 * 回调接口  
 */
interface CallBack {  
    /** 
     * 小高知道答案后告诉小刘时需要调用的方法,即回调方法
     * @param result 是问题的答案 
     */  
    public void answer(String result);  
}
/** 
 * 小刘类:实现了回调接口CallBack(背景一) 
 */  
class Liu implements CallBack {  
    /** 
     * 包含小高对象的引用 (背景二) 
     */  
    private Gao gao;   
 
    public Liu(Gao gao){  
        this.gao = gao;  
    }  
      
    /** 
     * 小刘通过这个方法去问小高 
     * @param question  小刘问的问题“学习Java选哪家机构呢?” 
     */  
    public void askQuestion(String question){  
        //小刘问小高问题
        gao.execute(Liu.this, question);          
    }  
    /** 
     * 小高知道答案后调用此方法告诉小刘
     */  
	@Override
	public void answer(String result) {
		System.out.println("小高告诉小刘的答案是:" + result);  		
	}  
} 
/** 
 * 小高类 
 */  
class Gao {
	 /** 
     * 相当于class B有一个参数为CallBack的方法C(背景三) 
     */  
    public void execute(CallBack callBack, String question){  
        System.out.println("小刘问的问题是:" + question);  
        //模拟小高挂点后先办自己的事情花了很长时间  
        try {
			Thread.sleep(10000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
        //小高办完自己的事情后想到了答案 
        String result = "学Java当然去北京尚学堂";  
        //小高打电话把答案告诉小刘,相当于class B 反过来调用class A 的D方法 
        callBack.answer(result);         
    }  
}

public class Test {
	public static void main(String[] args) {
		Gao  gao= new Gao();  
	    Liu liu = new Liu(gao);  
	    //小刘问问题
	    liu.askQuestion("学习Java选哪家机构呢?"); 
	} 
}

Inner Class 内部类

一般情况,我们把类定义成独立的单元。有些情况下,我们把一个类放在另一个类的内部定义,称为内部类(innerclasses)。

内部类可以使用public、default、protected 、private以及static修饰。而外部顶级类(我们以前接触的类)只能使用public和default修饰。

内部类的作用:

  1. 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。

  2. 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。 但外部类不能访问内部类的内部属性。

  3. 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整。

内部类的使用场合:

  1. 由于内部类提供了更好的封装特性,并且可以很方便的访问外部类的属性。所以,在只为外部类提供服务的情况下可以优先考虑使用内部类。

  2. 使用内部类间接实现多继承:每个内部类都能独立地继承一个类或者实现某些接口,所以无论外部类是否已经继承了某个类或者实现了某些接口,对于内部类没有任何影响。


泛型

泛型的本质就是“数据类型的参数化”。 我们可以把“泛型”理解为数据类型的一个占位符(形式参数),即告诉编译器,在调用泛型时必须传入实际类型。

泛型E像一个占位符一样表示“未知的某个数据类型”,我们在真正调用的时候传入这个“数据类型”。

class MyCollection<E> {// E:表示泛型;
	Object[] objs = new Object[5];

	public E get(int index) {// E:表示泛型;
		return (E) objs[index];
	}
	public void set(E e, int index) {// E:表示泛型;
		objs[index] = e;
	}
}
      
public class TestGenerics {
	public static void main(String[] args) {
		// 这里的”String”就是实际传入的数据类型;
		MyCollection<String> mc = new MyCollection<String>();
		mc.set("aaa", 0);
		mc.set("bbb", 1);
		String str = mc.get(1); //加了泛型,直接返回String类型,不用强制转换;
		System.out.println(str);
	}
}