本文主要是我在看《疯狂Java讲义》时的读书笔记,阅读的比较仓促,就用 markdown 写了个概要。
Java SE:(Java Platform, Standard Edition)整个Java技术的核心和基础,它是Java ME和Java EE编程的基础。
Java ME:(Java Platform, Micro Edition)主要用于控制移动设备和信息家电等有限存储的设备。
Java EE:(Java Platform,Enterprise Edition)提供了企业应用开发相关的完整解决方案,是Java技术中应用最广泛的部分。
JVM:(Java Virtual Machine)Java虚拟机,负责解释执行字节码文件。(JVM是Java程序跨平台的关键)
1、Java程序的组织形式
Java是一种纯粹的面向对象的程序设计语言,即必须以类(class)的形式存在。类是Java程序的最小程序单位。(Java程序不允许可执行语句、方法等成分独立存在,所有的程序部分都必须放在类定义里)
Java程序的入口是一个类中的main方法:public static void main(String[] args)
2、源文件的命名规则
3、垃圾回收机制
Java不需要程序员直接控制内存回收,程序的内存分配和回收都是由JRE在后台自动进行的。JRE会负责回收那些不再使用的内存,这种机制称为垃圾回收(Garbage Collection)。
Java中的所有关键字(都是小写):
除了上面48个关键字之外,Java还包含goto
和const
两个保留字(未来可能用作关键字)与 三个特殊的字面值:true
、false
、null
。
引用类型就是对一个对象的引用。实际上,引用类型变量就是一个指针,只是Java语言里不再使用指针这个说法。
1、数组的定义与初始化
在Java中,数组也是一种数据类型,而且是一种引用数据类型。
定义数组:
type[] arrayName; // 建议使用这种形式 type arrayName[];
注意:定义一个数组时,仅仅是定义了一个引用变量(也就是一个指针),它还未指向任何有效的内存,因此,定义数组时不能指定数组的长度。
2、数组在内存中的运行机制
数组是一种引用数据类型,所以数组变量只是一个引用。通过这个引用访问它所指向的有效内存(数组对象本身)。
通常,如果数组引用变量是一个局部变量,它会被存储在栈(stack)内存中,而实际的数组对象被存储在堆(heap)内存中,如下图所示:
下面看一个例子:
int[] a = {1,2,3}; int[] b = new int[4]; b = a;
从上面的例子可以看出,Java的引用类型就相当于C/C++中的指针类型。
3、栈内存与堆内存
栈内存:当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,内存栈也将自然销毁。
堆内存:当我们在程序中创建(new)一个对象时,该对象会被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收器才会在合适的时候回收它。
操作数组的工具类:java.util.Arrays
定义类:
[修饰符] class 类名 { 零到多个构造器定义 零到多个Field 零到多个方法 }
其中的修饰符可以是public
、final
、abstract
或省略。
类也是引用数据类型,用类定义的变量也只是一个引用(或者说指针),里面只是存放了一个地址值。
static
修饰的方法不能直接访问没有static
修饰的成员。(理由很简单:静态成员是独立于具体对象而存在,属于类本身,而非静态成员是依赖于具体的对象的)
Java里方法的参数传递方式只有一种:值传递。基本数据类型和引用数据类型都是将实参的一个副本传给形参,只不过引用数据类型拷贝的是地址值!
可变参数函数:
JDK 1.5之后,Java允许为方法指定数量不确定的形参,通过在最后一个形参的类型后增加三个点(…),代码实例如下:
public class MyClass { /** * 可变参数的方法 */ public static void func(int a, String... str) { for(String s : str) { System.out.println(s); } System.out.println(a); } /** * 程序入口-main */ public static void main(String[] args) { func(15, "第一个字符串","第二个字符串","第三个字符串"); } }
当然,如果你觉得这样麻烦,可以直接用一个数组代替。
方法重载:
Java允许同一个类里定义多个同名方法,只要形参列表不同就行。
在Java中,根据变量定义位置的不同,可以将变量分为两大类:成员变量 和 局部变量
1、成员变量的初始化和内存中的运行机制
2、局部变量的初始化和内存中的运行机制
类的封装
封装是面向对象的三大特征之一。为了实现良好的封装,需要:
Java中提供了4个访问控制级别:private
、protected
、public
和不加任何访问控制符(default
),它们的访问控制级别由小到大:
package、import 和 import static
包(package):为了解决类的命名冲突,Java引入了包机制,提供了类的多层命名空间。
如果一个类被放于某个包中,则我们应该在该Java源文件的第一个非注释行添加如下代码:
package packageName;
import
语句可以导入指定包下某个类或全部类,但import
语句并不是必需的,只要坚持在类里面使用其他类的全名,则可以无须使用import
语句。
注意:在JDK 1.5以后增加了一种静态导入(import static)的语法,用于导入指定类的某个静态 Field、方法或该类全部的静态 Field、方法。
import static package.subpackage...className.fieldName; // 导入某一静态变量 import static package.subpackage...className.methodName; // 导入某一静态方法 import static package.subpackage...className.*; // 导入该类的所有静态Field、方法
用一句话归纳import
和import static
的作用:使用import
可以省略写包名,而使用import static
则可以连类名都省略。
Java的常用包
Java的核心类都放在java
这个包及其子包下,Java扩展的许多类都放在javax
包及其子包下。下面几个包是Java语言中的常用包:
java.lang
:这个包下包含了Java语言的核心类,如 String、Math、System 和 Thread 类等,使用这个包下的类无须使用 import 语句导入,系统会自动导入这个包下的所有类。java.util
:这个包下包含了Java的大量工具类/接口和集合框架类/接口,例如 Arrays、List 和 Set 等。java.net
:这个包下包含了一些Java网络编程相关的类/接口。java.io
:这个包下包含了一些Java输入/输出编程相关的类/接口。java.text
:这个包下包含了一些Java格式化相关的类。java.sql
:这个包下包含了Java进行 JDBC 数据库编程的相关类/接口。java.awt
:这个包下包含了抽象窗口工具集(Abstract Window Toolkits)的相关类/接口,这些类主要用于构建 GUI 程序。java.swing
:这个包下包含了 Swing 图形用户界面编程的相关类/接口,这些类可用于构建平台无关的 GUI 程序。构造器
构造器也就是构造函数!!!
Java类可以包含一个或一个以上的构造器。一旦程序员提供了自定义的构造器,系统就不再提供默认的无参构造器了。(所以如果为一个类编写了有参数的构造器,通常建议为该类也额外提供一个无参数的构造器)
类的继承
继承是面向对象的三大特征之一。Java的继承具有单继承的特点,每个子类只有一个直接父类。
继承的语法格式:
修饰符 class Derive extends Base { // 类定义部分 }
Java使用extends
作为继承的关键字,extends
在英文中是扩展的意思。
重写父类的方法要遵循“两同两小一大”的规则:
如果需要在子类方法中调用父类中被覆盖的方法,若被覆盖的是实例方法,使用super
作为调用者;若被覆盖的是类方法,使用父类类名
作为调用者。
构造器的执行顺序
子类不会获得父类的构造器,但子类构造器里可以调用父类构造器。有如下几种情况:
super
显式调用父类构造器。this
显示调用本类中重载的构造器,执行本类中另一个构造器时即会调用父类构造器。super
调用,也没有this
调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。不管上面哪种情况,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;不仅如此,执行父类构造器时,系统会再次上溯执行其父类构造器……依此类推,创建任何Java对象,最先执行的总是java.lang.Object
类的构造器。
多态
多态是面向对象的三大特征之一。
Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时的类型决定,运行时类型由实际赋给该变量的对象决定。两个类型不一致时,就可能出现多态。
当把一个子类对象直接赋给父类引用变量时,这个引用变量的编译时类型是 BaseClass,而运行时类型是 SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征。也就是说:相同类型的变量 调用同一个方法时,呈现出多种不同的行为特征,这就是多态。
多态的两个前提: 要有继承(inheritance),要有方法重写(override)。
instanceof 运算符
instanceof
是Java中的一个二元运算符,它的作用是在运行时判断左边对象是否是右边类(或其子类)的实例。如果是,返回true
,否则返回false
。
public class MyClass { public static void main(String[] args) { String str = ""; // str 是String类型引用变量 Object obj = ""; // obj 的编译时类型是 Object,但实际类型是 String System.out.println("str 是 String 的实例:" + (str instanceof String)); // true System.out.println("str 是 Object 的实例:" + (str instanceof Object)); // true System.out.println("obj 是 String 的实例:" + (obj instanceof String)); // true System.out.println("obj 是 Object 的实例:" + (obj instanceof Object)); // true System.out.println("obj 是 Math 的实例:" + (obj instanceof Math)); // false } }
需要注意:instanceof
运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。
instanceof
运算符的常用之处:在进行强制类型转换之前,首先判断前一个对象是否是后一个类的实例,是否可以成功转换,从而保证代码更加健壮。
初始化块
初始化块是Java类里可出现的第4种成员(前面依次有 Field、方法和构造器)。与构造器的作用类似,初始化块也可以对Java对象进行初始化操作。
初始化块的语法格式如下:
[修饰符] { // 可执行代码 ... }
初始化块的修饰符只能是static
,使用 static 修饰的初始化块被称为静态初始化块。
一个类里可以有多个初始化块,先定义的初始化块先执行,后定义的初始化块后执行,下面是一个例子:
public class MyClass { { int a = 5; System.out.println("第一个初始化块!"); } { System.out.println("第二个初始化块!"); } public MyClass() { System.out.println("类的无参数构造器!"); } public static void main(String[] args) { new MyClass(); // 创建一个对象 } }
控制台输出结果:
第一个初始化块! 第二个初始化块! 类的无参数构造器!
可以看出,初始化块是在构造器之前执行的。创建一个 Java 对象时,不仅会执行该类的普通初始化块和构造器,而且系统会一直上溯到java.lang.Object
类,先执行 java.lang.Object 类的初始化块,开始执行 java.lang.Object 的构造器,依次向下执行其父类的初始化块,开始执行其父类的构造器……最后才执行该类的初始化块和构造器,返回该类的对象。
虽然 Java 允许一个类中定义多个的普通初始化块,但这没有任何意义,所以如果要使用初始化块的话定义一个就行了。
初始化块和构造器的区别
初始化块总是在构造器之前执行。虽然它们的作用非常相似,但依然存在一些差异的。
与构造器不同的是,初始化块是一段固定执行的代码,它不能接受任何参数。因此,如果有一段初始化的代码对所有对象完全相同,且无须接收任何参数,就可以把这段初始化代码提取到初始化块中。
通过把多个构造器中的相同代码提取到初始化块中,能更好地提高初始化代码的复用,提高整个应用的可维护性。
静态初始化块
初始化块的修饰符只能是static
,使用 static 修饰的初始化块被称为静态初始化块。
静态初始化块,也属于类的静态成员,因此静态初始化块不能访问非静态成员(包括实例Field和实例方法)。静态初始化块用于对整个类进行初始化处理,通常用于对类Field执行初始化处理。
系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。因此,静态初始化块总是比普通初始化块先执行。与普通初始化块类似的是,系统在类初始化阶段不仅会执行本类的静态初始化块,还会一直上溯到 java.lang.Object 类(如果它包含静态初始化块),从上往下依次执行其父类的静态初始化块……最后才执行该类的静态初始化块。经过这个过程,才完成了该类的初始化。而只有类完成初始化以后,才可以在系统中使用这个类,包括访问这个类的类Field、类方法,或者用这个类来创建实例。
包装类
Java 是面向对象的编程语言,但它也包含了 8 种基本数据类型。基本数据类型的数据不具备“对象”的特性:没有Field、方法可以被调用。
所有引用类型的变量都继承了Object
类,都可当成 Object 类型变量使用,但基本数据类型的变量却不可以。为了解决这个问题,Java 提供了包装类(Wrapper Class),可以把 8 个基本类型的值包装成对象使用。
把基本数据类型变量 包装成 对应的包装类对象 是通过对应包装类的构造器来实现的,不仅如此,8个包装类中除了 Character 之外,还可以通过传入一个字符串来构建包装类对象。
public class MyClass { public static void main(String[] args) { boolean b1 = true; int i1 = 5; Boolean _b = new Boolean(b1); Integer _i = new Integer(i1); Float _f = new Float("3.14"); // 取出包装类对象里的值 boolean b2 = _b.booleanValue(); int i2 = _i.intValue(); float f2 = _f.floatValue(); } }
可能你会觉得,这样的转换有些繁琐。但从 JDK 1.5 开始提供了自动装箱(Autoboxing) 和 自动拆箱(AutoUnboxing)功能,即可以把一个基本类型变量直接赋给对应的包装类变量或 Object 变量(自动装箱),也可以把包装类对象直接赋给一个对应的基本类型变量。
基本类型变量与字符串的转换
toString( )方法
toString()
方法是 Object 类里的一个实例方法,而所有的 Java 类都是 Object 类的子类,因此所有的 Java 对象都具有 toString() 方法。
不仅如此,所有的 Java 对象都可以和字符串进行连接运算,也可以使用System.out.println()
进行输出。当进行上面的操作时,系统会自动调用 Java 对象的 toString()
方法,使用其返回的字符串。
Object 类的toString
方法是一个“自我描述”的方法,它总是返回该对象实现类的“类名@hashCode”值。但是这个返回值并不能真正实现“自我描述”的功能,这时可以对这个方法进行重写。
==和equals的区别
Java 程序中判断两个变量是否相等有两种方式:一种是使用==
运算符,另一种是使用equals
方法。
equals
方法,只能使用==
判断两个变量的值是否相等。==
运算符是判断两个引用变量是否指向内存中的同一个对象,也就是比较对象的内存地址;而equals
是比较两个对象的值是否相等(String类,Integer类等等)。需要知道的是,equals 方法是 Object 类的一个实例方法。在 Object 类中equals
方法和==
没有任何区别,都是判断两个变量是否指向同一个对象。而String类,Integer类等等一些类,是重写了equals方法,才使得equals和“==”不同。
所以,当自己创建类时,想要自定义相等的标准,必须重写equals方法。
final修饰符
Java 提供了final
关键字来修饰变量、方法和类。系统不允许为 final
变量重新赋值,子类不允许覆盖父类的final
方法,不允许继承final
类。
final 成员变量必须由程序员显式地指定初始值,系统不会对 final 成员变量进行隐式初始化。对于 final 修饰的类 Field,必须在声明该Field时或在静态初始化块中指定初始值;对于 final 修饰的实例 Field,必须在声明该Field时、普通初始化块或构造器中指定初始值。
前面说过,系统不会为局部变量执行隐式初始化,必须由程序员显式指定。对于 final 修饰的局部变量,可以在声明时指定初始值,也可以在后面的代码中对其赋值,但只能一次。
final 修饰基本类型变量时,表示变量的值不能被改变;final 修饰引用类型变量时,表示该变量所引用的地址不能被改变,即一直引用同一个对象,但这个对象是可以改变的。
当定义 final 变量时就为该变量指定了初始值,而且该初始值可以在编译时就被确定下来,那么这个变量就变成了“宏变量”。编译器会把程序中所有用到该变量的地方直接替换成该变量的值。
抽象类与抽象方法
Java 中使用abstract
修饰符来定义抽象类和抽象方法。有抽象方法的类必须定义成抽象类,但抽象类里可以没有抽象方法。
抽象类不能被实例化,只能当作父类被其他子类继承。抽象方法没有函数体,必须由子类提供实现(即重写)。
与abstract
不能同时使用的关键字:
final
和abstract
不能同时使用,因为它们是对立的。static
和abstract
不能同时修饰某个方法,因为如果一个抽象方法被定义成静态方法,通过类名调用该方法将出现错误。private
和abstract
不能同时修饰某个方法,因为抽象方法必须被子类重写才有意义,而子类不能访问和重写父类的 private 方法。接口(interface)
上面说到,抽象类既可以包含抽象方法,也可以普通方法。而接口(interface)是一种更彻底的抽象,接口里的所有方法都是抽象方法。
接口定义的是多个类共同的公共行为规范,故它里面通常是定义一组公用方法。基本语法如下:
[修饰符] interface 接口名 extends 父接口1,父接口2... { 零个到多个常量定义... 零个到多个抽象方法定义... }
修饰符可以是 public 或者省略,如果省略了 public 访问控制符,则默认采用包权限访问控制符。另外,与类继承不同的是,接口继承中一个接口可以有多个直接父接口(接口只能继承接口而不能继承类)。
由于接口是一种规范,因此接口里不能包含构造器和初始化块。接口里可以包含3种成员: Field(只能是常量)、方法(只能是抽象方法)、内部类(包括内部接口、枚举)。
public static final
修饰符来修饰,不管定义时是否指定。public abstract
修饰符修饰,不管定义方法时是否指定。public static
修饰符修饰,不管定义时是否指定。接口不能用于创建实例,其主要用途是被实现类实现。实现使用implements
关键字:
[修饰符] class 类名 extends 父类 implements 接口1,接口2... { // 类体部分 }
一个类只能有一个直接父类,但一个类可以实现多个接口。实现接口与继承父类相似,也可以获得所实现的接口里定义的成员,因此可以把实现接口理解为一种特殊的继承。
接口与抽象类的比较
相同点:
不同点:
内部类
在Java类里只能包含5种成员:Field、方法、构造器、初始化块、内部类(包括接口和枚举类)。前四种类成员已经介绍过了,下面介绍一下内部类。
内部类也叫嵌套类,语法格式:
public class OuterClass { // 此处可以定义内部类 }
通常,我们把内部类作为成员内部类来定义,而不是作为局部内部类(在方法里定义的内部类)。
非静态内部类里不允许定义静态成员,而静态内部类里可以定义静态成员,也可以定义非静态成员。
根据静态成员不能访问非静态成员的规则,外部类的静态方法不能使用非静态内部类,静态内部类也不能访问外部类的非static成员。
枚举类
枚举类是一种不能自由创建对象的类,它的对象在定义类时已经固定下来。枚举类特别适合定义像行星、季节这样的类,它们能创建的实例是有限且确定的。
在 Java 1.5 以前,要定义一个枚举类,必须手动去实现。下面就是一个 Season 枚举类的例子:
public class Season { private final String name; private final String description; public static final Season SPRING = new Season("春天","春暖花开"); public static final Season SUMMER = new Season("夏天","夏日炎炎"); public static final Season FALL = new Season("秋天","秋高气爽"); public static final Season WINTER = new Season("冬天","围炉赏雪"); // 私有化构造器 private Season(String name, String description) { this.name = name; this.description = description; } // 只为两个 Field 提供 getter 方法 public String getName() { return name; } public String getDescription() { return description; } }
上面的 Season 类是一个不可变类,它只能创建4种对象,可以通过Season.SPRING
的方式来取得 Season 对象。
Java 1.5 新增了一个enum
关键字,用以定义枚举类:
public enum Season { SPRING,SUMMER,FALL,WINTER; }
枚举类(enum)是一种特殊的类,它一样可以有自己的 Field、方法和构造器,可以实现一个或者多个接口。因为它特殊,所以有几点需要注意:
enum
定义的枚举类默认继承了java.lang.Enum
类,而不是继承 Object 类。enum
定义的非抽象的枚举类默认使用 final 修饰,因此枚举类不能派生子类。private
访问控制符修饰。public static final
修饰。