Java&Android 基础知识梳理(0) - Java 基础知识大纲

Java 基础知识大纲

  • 一、面向对象
  • 二、Object类相关
  • 三、重要关键字
  • 四、内部类
  • 五、抽象类 & 接口
  • 六、编码
  • 七、异常
  • 八、注解
  • 九、容器
  • 十、内存区域
  • 十一、垃圾回收
  • 十二、类加载
  • 十三、泛型
  • 十四、反射

一、面向对象

1.1 对 Java 多态的理解

面向对象编程的三大特性:封装、继承、多态。

  • 封装:隐藏类的内部实现机制。
  • 继承:重用父类代码,为多态做铺垫。
  • 多态:程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定。

实现多态的三个必要条件:继承、重写、向上转型。

  • 继承:在多态中必须存在有继承关系的子类和父类。
  • 重写:子类对父类中的某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
  • 向上转型:将父类引用指向子类对象,只有这样,该引用才具备调用子类方法的能力。

实现形式:

  • 基于继承实现的多态。
  • 基于接口实现的多态。

1.2 父类静态方法能不能被子类重写

结论

父类的静态方法可以被子类继承,但是不能被子类重写。

当子类声明了一个与父类相同的静态方法时,只能称为隐藏。

  • 父类
/**
 * @author lizejun
 **/
public class Parent {
    
    public static void staticMethod() {
        System.out.println("Parent Static Method");
    }
    
    public void method() {
        System.out.println("Parent Method");
    }
}
  • 子类
/**
 * @author lizejun
 **/
public class Child extends Parent {

    public static void staticMethod() {
        System.out.println("Child Static Method");
    }

    public void method() {
        System.out.println("Child Method");
    }
}
  • 示例。
/**
 * @author lizejun
 */
public class MainApp {

    public static void main(String[] args) {
        Parent parent = new Child();
        parent.method();
        parent.staticMethod();
        
        Child child = new Child();
        child.method();
        child.staticMethod();
    }
}
  • 运行结果。
Child Method
Parent Static Method
Child Method
Child Static Method

二、Object 类相关

2.1 Java 中 ==、equals 和 hashCode 的区别

==

Java中,分为基本数据类型和复合数据类型,基本数据类型包括byteshortcharintlongfloatdoubleboolean这八种。

  • 对于基本数据类型,==比较的是它们的值。
  • 对于复合数据类型,比较的是它们在内存中的存放地址,即比较的是否是同一个对象。

equals

Objectequals默认的实现是比较两个对象是不是==,和==的效果是相同的。

    public boolean equals(Object obj) {
        return (this == obj);
    }

而有些时候,对于两个不同的对象,我们又需要提供 逻辑 上是否相等的判断方法,这时候就需要重写equals方法。Java提供的某些类已经重写了equals方法,用于判断"相等"的逻辑,例如Integer

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

hashCode

hashCode的目的是用于在对象进行散列的时候作为key输入,保证散列的存取性能。Object的默认hashCode实现为在对象的内存地址上经过特点的算法计算出。
由此可见,equalshashCode的其实没有什么关系。但是由于HashSet/HashMap容器的存在,又需要保证:

  • 对于equals相等两个对象,其hashCode返回的值一定相等
  • 对于equals不同的对象要尽量做到hashCode不同。

Java&Android 基础知识梳理(8) - 容器类 中提到的HashMap的实现,value替换的条件是判断key

//Value 替换的条件
//条件1:hash 值完全相同
//条件2:key 指向同一块内存地址 或者 key 的 equals 方法返回为 true
(e.hash == hash && ((k = e.key) == key || key.equals(k)))

假如我们只重写了equals方法,而没有重写hashCode方法,就会导致逻辑上相等的两个key,放在了容器中的不同位置。

2.2 Integer

存储原理

  • int属于基本数据类型,存储在栈中。
  • Integer属于复合数据类型,引用存储在栈中,引用所指向的对象存储在堆中。

缺省值

  • 0
  • null

泛型支持

泛型支持Integer,不支持int

int 与 Integer 之间的比较

//基本数据类型。
int a1 = 128;
//非 new 出来的 Integer。
Integer a2 = 128;
//new 出来的 Integer。
Integer a3 = new Integer(128);
  • new出来的Integernew出来的Integer不相等,前者指向存放它的常量池(数值位于-128127之间)或者堆,后者指向堆中的另外一块内存。
  • 两个都是非new出来的Integer,如果在-128127之间,返回的是true,否则返回的是false,因为Java在编译Integer a2 = 128的时候,会翻译成Integer.valueOf(128),而valueOf函数会对-128127之间的数进行缓存。
    public static Integer valueOf(int i) {
        //low = -128, high = 127.
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
  • 两个都是new出来的,返回false
  • intInteger相比,都为true,因为会把Integer自动拆箱为int再去比较。

2.3 String

2.3.1 new String 和直接赋值的区别

new String和直接赋值的区别:

  • String str1 = "ABC",可能创建一个或者不创建对象,如果ABC这个字符串在常量池中已经存在了,那么str1直接指向这个常量池中的对象。
  • String str2 = new String("ABC"),至少创建一个对象。一定会在堆中创建一个str2中的String对象,它的valueABC,如果ABC这个字符串在常量池中不存在,会在池中创建一个对象。

例子 1

String s ="a" + "b" + "c" + "d"

只创建了一个对象,在编译器在编译时优化后,相当于直接定义了一个abcd的字符串。

例子 2

String ab = "ab";                                          
String cd = "cd";                                       
String abcd = ab + cd;                                      
String s = "abcd";  

abcd存储的是两个常量池中的对象,当执行ab + cd时,首先会在堆中创建一个StringBuilder类,同时用ab指向的字符串对象完成初始化,然后调用append方法完成对cd指向字符串的合并操作,接着调用StringBuildertoString方法在堆中创建一个String对象,最后将刚生成的String对象的地址存放在局部变量abcd中。

2.3.2 String、StringBuilder、StringBuffer 的区别

对比

  • String中的是常量数组,只能被赋值一次。
  • 在编译阶段就能够确定的字符串常量,没有必要创建String/StringBuffer对象,直接使用字符串常量的+效率更高。
  • StringBuffer中的value[]是一个很普通的数组,而且可以通过append方法将新字符串加入到末尾,改变内容和大小。
  • StringBuffer允许多线程操作,其很多方法都被关键字synchronized修饰,而StringBuilder则不是,如果不考虑线程安全,StringBuilder应该是首选。

注意点

  • 不停地创建对象是程序低效的原因,因此我们应该尽可能保证相同的字符串在堆中只创建一个String对象。
  • 当调用Stringintern时,如果常量池已经有了当前String的值,那么返回这个常量指向的地址;如果没有,则将String值加入到常量池中。
  • String、StringBuffer、StringBuilder 详细对比

2.3.3 String 为什么要设计成不可变类

常量池的需要

字符串常量池是Java堆内存的一个特殊区域,当创建一个String对象时,假如字符串已经存在于常量池中,则不会创建新的对象,而是直接引用已经存在的对象。

String s1 = "abc";
String s2 = "abc";

s1s2指向常量池中的同一个对象abc,如果String是可变类,s1对其的修改将会影响到s2

HashCode 缓存的需要

因为字符串不可变,在创建的时候HashCode就被缓存,不需要重新计算。

多线程安全

由多个线程之间共享,不需要同步处理。

如何实现不可变

  • 私有成员变量
  • public的方法都是复制一份数据
  • Stringfinal,因此不可继承
  • 构造函数深拷贝,进行copy而不是直接将value[]赋值给内部变量。

2.4 序列化 & 反序列化

Java&Android 基础知识梳理(2) - 序列化

三、重要关键字

3.1 final

final可以用于以下四个地方:

  • 变量:静态和非静态
  • 定义方法的参数
  • 定义方法
  • 定义类

3.1.1 变量

静态变量

  • 如果final修饰的是一个基本类型,就表示这个变量被赋予的值是不可变的。
  • 如果final修饰的是一个对象,就表示这个变量被赋予的引用是不可变的。

非静态变量

final修饰的变量必须被初始化,初始化的方式有以下几种:

  • 在定义的时候初始化
  • 非静态final变量在初始化块中初始化,不可在静态初始化块中初始化
  • 静态final变量可以在静态初始化块中初始化
  • 非静态final变量可以在类的构造器中初始化,但是静态final变量不可以

3.1.2 方法

不可以被子类重写,但是不影响被子类继承。

3.1.3 类

不允许被继承。

3.2 static

四、内部类

4.1 定义

内部类的定义:在一个外部类的内部再定义一个类。

4.2 分类

  • 成员内部类:作为外部类的成员,可以直接使用外部类的所有成员和方法。
  • 静态内部类:声明为static的内部类,成员内部类不能有static数据和static方法,但嵌套内部类可以。
  • 局部内部类:内部类定义在方法和作用域内。只在该方法或条件的作用域内才能使用,退出作用域后无法使用。
  • 匿名内部类:匿名内部类有几个特点:不能加访问修饰符;当所在方法的形参需要被内部类里面使用时,该形参必须为final

4.3 作用

4.3.1 实现隐藏

外部顶级类即类名和文件名相同的只能使用publicdefault修饰,但内部类可以是staticpublic/default/protected/private

首先,定义内部类需要实现的接口:

/**
 * @author lizejun
 **/
public interface InnerInterface {
    void call();
}

再定义一个包装类:

/**
 * @author lizejun
 **/
public class Outer {

    private class InnerImpl implements InnerInterface {

        @Override
        public void call() {
            System.out.println("call inner");
        }
    }

    public InnerInterface getInnerInterface() {
        return new InnerImpl();
    }
}

由于我们将InnerInterface的实现类声明为了private,因此外部并不知道它的存在,也就达到了隐藏的目的。

/**
 * @author lizejun
 */
public class MainApp {

    public static void main(String[] args) {
        Outer outer = new Outer();
        InnerInterface inner = outer.getInnerInterface();
        inner.call();
    }
}

4.3.2 无条件地访问外部类当中的元素

/**
 * @author lizejun
 **/
public class Outer {
    
    //外部类的私有变量。
    private int outerSelfValue = 0;

    private class InnerImpl implements InnerInterface {

        @Override
        public void call() {
            //内部类可以无条件地访问。
            System.out.println("call inner, outerValue=" + outerSelfValue);
        }

    }

    public InnerInterface getInnerInterface() {
        return new InnerImpl();
    }
}

这仅限于非静态内部类,它和静态内部类的区别是:

  • 静态内部类没有指向外部的引用
  • 在任何非静态内部类中,都不能有静态变量、静态方法或者静态内部类。
  • 创建非静态内部类,必须要通过外部类来创建,例如Outer.InnerImpl outer = new Outer().new InnerImpl();;静态内部类则可以直接创建,Outer.InnerImpl outer = new Outer.InnerImpl();
  • 静态内部类只可以访问外部类的静态方法和静态变量。

4.3.3 实现多重继承

由于Java不允许多重继承,因此假如我们希望一个类同时具备其它两个类的功能时,就可以采用内部类来实现。

实现乘法的子类:

/**
 * @author lizejun
 **/
public class MultiCalculator {

    public int multi(int a, int b) {
        return a * b;
    }
}

实现加法的子类:

/**
 * @author lizejun
 **/
public class PlusCalculator {

    public int add(int a, int b) {
        return a;
    }
}
  • 通过内部类实现多重继承
/**
 * @author lizejun
 **/
public class Calculator extends PlusCalculator {

    class MultiCalculatorImpl extends MultiCalculator {

        @Override
        public int multi(int a, int b) {
            return super.multi(a, b);
        }
    }

    public int multi(int a, int b) {
        return new MultiCalculatorImpl().multi(a, b);
    }
}

4.3.4 避免修改接口而实现同一个类中两种同名方法的调用

用于解决下面的困境:一个需要继承另一个类,还要实现一个接口,而继承的类和接口里面有两个同名的方法。那么我们调用该方法的时候,究竟是父类的,还是实现的接口呢,这时候就可以使用内部类来解决这一问题。

  • 需要继承的子类中有call方法
/**
 * @author lizejun
 **/
public class BaseOuter {

    public void call() {
        System.out.println("call baseOuter");
    }
}
  • 需要实现的接口,同样有call方法
/**
 * @author lizejun
 **/
public interface InnerInterface {
    void call();
}
  • 采用内部类的方式避免出现困惑
/**
 * @author lizejun
 **/
public class Outer extends BaseOuter {

    private class InnerImpl implements InnerInterface {

        @Override
        public void call() {
            //内部类可以无条件地访问。
            System.out.println("call inner");
        }

    }

    public InnerInterface getInnerInterface() {
        return new InnerImpl();
    }
}
  • 调用方式
/**
 * @author lizejun
 */
public class MainApp {

    public static void main(String[] args) {
        Outer outer = new Outer();
        //1. 调用的是继承父类的接口。
        outer.call();
        //2. 调用的实现接口的方法。
        outer.getInnerInterface().call();
    }
}

4.4 应用场景

幕后英雄的用武之地——浅谈 Java 内部类的四个应用场景

  • 除了它的外部类,不再被其它的类使用
  • 解决一些非面向对象的语句块
/**
 * @author lizejun
 **/
public interface InnerWorker {
    void work();
}
/**
 * @author lizejun
 **/
public class Factory {

    public void doWork(InnerWorker worker) {
        try {
            worker.work();
        } catch (Exception exception) {
            System.out.println("exception!");
        } finally {
            System.out.println("finally!");
        }
    }
}
/**
 * @author lizejun
 */
public class MainApp {

    public static void main(String[] args) {
        Factory factory = new Factory();
        factory.doWork(new InnerWorker() {

            @Override
            public void work() {
                System.out.println("work1 work");
            }
        });
        factory.doWork(new InnerWorker() {

            @Override
            public void work() {
                System.out.println("work2 work");
            }
        });
    }
}
  • 一些多算法场合
  • 适当使用内部类,使得代码更加灵活和具有扩展性
/**
 * @author lizejun
 **/
public interface Shape {
    void draw();
}
/**
 * @author lizejun
 **/
public abstract class ShapeFactory {

    private static HashMap<String, ShapeFactory> factories = new HashMap();

    public static void addFactory(String id, ShapeFactory factory) {
        factories.put(id, factory);
    }

    public static Shape createShape(String id) {
        if (!factories.containsKey(id)) {
            try {
                Class.forName(id);
            } catch (Exception e) {}
        }
        return factories.get(id).create();
    }

    protected abstract Shape create();

}
/**
 * @author lizejun
 */
public class MainApp {

    public static void main(String[] args) {
        Shape shape = ShapeFactory.createShape(Circle.ID);
        shape.draw();
    }
}

4.5 内部类和闭包

闭包就是把函数以及变量包起来,使得变量的生存周期延长,闭包跟面向对象是一棵树上的两条枝,实现的功能是等价的。

涉及到闭包的两种内部是:局部内部类和匿名内部类。当它们引用外部变量时,外部的变量需要是final的。

以下面这个例子为例,定义一个内部类的接口:

/**
 * @author lizejun
 **/
public interface InnerInterface {
    void call();
}
/**
 * @author lizejun
 **/
public class InnerClose {
    
    public void doClose(final int a) {
        InnerInterface inner = new InnerInterface() {
            
            @Override
            public void call() {
                System.out.println("a=" + a);    
            }
        };
        inner.call();
    }
}
/**
 * @author lizejun
 */
public class MainApp {

    public static void main(String[] args) {
        InnerClose close = new InnerClose();
        close.doClose(1);
    }
}

在编译之后,局部内部类会生成独立的InnerClose$1.class文件,而变量a是方法级别的,方法运行完变量就销毁了,而局部内部类对象还可能一直存在,不会随着方法运行结束就马上被销毁。这时候就会出现,局部内部类对象需要访问一个已经不存在的局部变量a

因此,通过将变量声明为final,编译器会将final局部变量复制一份,复制品作为局部内部类中的成员,这样,当局部内部类访问局部变量时,其实真正访问的是这个局部变量的复制品。

由于被final修饰的变量赋值后不能再修改,所以就保证了复制品与原始变量的一致,就好像是局部变量的 生命期变长了,这就是Java的闭包。

五、抽象类 & 接口

5.1 区别

  • 抽象类和接口都不能被实例化。
  • 抽象类要被子类继承,接口要被类实现。
  • 接口只能做方法的声明,抽象类可以做方法的声明,也可以做方法的实现。
  • 接口里定义的变量只能是公共的静态常量,抽象类中的变量可以是普通变量。
  • 抽象类里的抽象方法必须全部被子类实现;接口的接口方法必须全部被子类实现,否则只能为抽象类。
  • 抽象类里可以没有抽象方法。
  • 如果一个类里有抽象方法,那么这个类只能是抽象类。
  • 抽象方法要被实现,所以不能是静态的,也不能是私有的。
  • 接口可继承接口,并可多继承接口,但类只能单继承。

5.2 应用场景

抽象类

在既需要统一的接口,又需要实例变量或缺省方法的情况下,可以使用:

  • 定义了一组接口,但又不想强迫每个实现类都必须实现所有的接口。
  • 某些场合下,只靠纯粹的接口不能满足类与类之间的协调,还需要类中表示状态的变量来区别不同的关系。
  • 规范了一组相互协调的方法,其中一些方法是共同的,与状态无关的,可以共享的,无需子类分别实现;而另一些方法却需要各个子类根据自己特定的状态来实现特定的功能。

接口

  • 类与类之间需要特定的接口协调,而不在乎其如何实现。
  • 需要将一组类视为单一的类,而调用者只通过接口来与这组类发生联系。

六、编码

6.1 为什么要编码

  • 计算机中存储信息的最小单元是8bit,所以能表示的字符范围是0~255个。
  • 要表示的符号太多,无法用一个字节来完全表示。
  • 要解决这个矛盾必须要一个新的数据结构char,从charbyte必须编码。

6.2 编码方式

ASCII 码

ASCII码总共有128个,用一个字节的低7位表示。

ISO-8859-1

ASCII码基础上制定了一系列标准来扩展ASCII编码,其仍然是单字节编码,总共能表示256个字符。

GB2312

双字节编码,总的范围是A1~F7,从A1~A9是符号区,总共包含682个符号;从B0~F7是汉字区,包含6763个汉字。

GBK

扩展GB2312,加入更多的汉字,其编码范围是8140~FEFE,和GB2312兼容。

GB18030

我国的强制标准,可能是单字节、双字节或者四字节编码,与GB2312兼容。

Unicode 编码集

ISO试图创建一个全新的语言字典,将所有的语言互相翻译。String在内存中 不需要编码格式,它只是一个Unicode字符串而已。只有当字符串需要在网络中传输或要被写入文件时,才需要编码格式。

  • UTF-16
    UTF-16具体定义了Unicode字符在计算机中的存取方法,它用两个字节表示Unicode转化格式。
  • UTF-8
    UTF-16的缺点在于很大部分字符仅用一个字节就可以表示,目前却需要使用两个,而UTF-8采用了变长技术,不同类型的字符可以由1~6个字节组成。
    • 如果一个字节,最高位为0,表示这是一个ASCII字符。
    • 如果一个字节,以11开头,连续的1个数表示这个字符的字节数。
    • 如果一个字节,以10开始,表示它不是首字节,需要向前查找才能得到当前字符的首字节。
String s = "严"; 
//编码。
byte[] b = s.getBytes("UTF-8");
//解码。 
String n = new String(b,"UTF-8"); 

6.3 对比

  • GB2312GBK编码规则类似,但是GBK范围更大,它能处理所有汉字字符。
  • UTF-16UTF-8都是处理Unicode编码,UTF-16效率更高,它适合在本地磁盘和内存之间使用。
  • UTF-16不是在网络之间传输,因为网络传输容易损坏字节流,UTF-8更适合网络传输,对ASCII字符采用单字节存储,单字节损毁不会影响后面其它字符。

6.4 参考文章

(1) Java 中的字符编码方式
(2) Java 几种常见的编码格式

七、异常

Java中定义了许多异常类,并定义了Throwable作为所有异常的超类,将异常划分为两类ErrorException

  • Error:程序中无法处理的错误,例如NoClassDefFoundErrorOutOfMemory等,当此类错误发生时,JVM将终止进程。
  • Exception:程序本身可以处理的异常。
    • 运行时异常,RuntimeException及其子类,表示JVM在运行时可能出现的错误,例如空指针、数组越界等,一般是由逻辑错误引起。
    • 受检异常:除RuntimeException及其子类的异常。编译器会检查此类异常,并提示你处理本类异常 - 要么使用try-catch捕获,要么使用throws语句抛出,否则编译不通过。

八、注解

Java&Android 基础知识梳理(1) - 注解

九、容器

Java&Android 基础知识梳理(8) - 容器类

十、内存区域

Java&Android 基础知识梳理(3) - 内存区域

十一、垃圾回收

Java&Android 基础知识梳理(4) - 垃圾收集器与内存分配策略

十二、类加载

Java&Android 基础知识梳理(5) - 类加载&对象实例化

十三、泛型

Java & Android 基础知识梳理(12) - 泛型

十四、反射

Java&Android 基础知识梳理(13) - 反射