理解Java的类与对象
1. 类
类是构造对象的模板,它定义了对象所拥有的全部属性。
一个类通常需要包含以下一些元素:
- 变量:用于存储数据。
- 构造器:用来构造并初始化对象。
- 方法:即操纵数据的过程。
当然,可以在类中定义新的类,即。
2. 变量与对象
首先变量与对象是两个不同的概念:
ArrayList<String> list; // 定义了一个String类型的变量str new ArrayList<String>(); // 实例化了一个对象
在第一行中我们定义了一个ArrayList类型的变量list(需要初始化),而第二行中用new关键字实例化了一个ArrayList类的实例。
我们可以利用第二行所构造的对象对list进行初始化:
ArrayList<String> list = new ArrayList<String>();
上述新的语句定义了一个ArrayList类型的变量list,并让它引用了一个新构造的对象。
同样也可以让变量引用一个已存在的变量完成初始化:
ArrayList<String> list1 = list;
这样,list和list1就共同指向了一个对象。
可以把它们看成是两个管理者,共同管理同一个对象,只要有一个变量修改了对象的状态,另外一个变量指向的对象的状态也发生了改变(对象是同一个,只是有两个变量同时引用了它):
ArrayList<String> list = new ArrayList<String>(); ArrayList<String> list1 = list; list.add("1. add in list"); System.out.println(list1); list1.add("2. add in list1"); System.out.println(list); /*输出: *[1. add in list] *[1. add in list, 2. add in list1] */
3. 类中的变量
Java的类中的变量总体上可以分成三部分:
- 成员变量(实例变量)
- 类变量(静态变量)
- 局部变量
3.1 成员变量(实例变量)
- 变量的定义: 成员变量定义在类内部,在实例化之后,这部分的变量就组成了对象的实例域。
- 修饰词: 成员变量可以用访问权限修饰词(private、public、protected)限定其访问权限,通常我们将其限定为private。 可以将实例域定义为final,被final关键字修饰的成员变量在构造器执行之后就不能再被修改,即常量。对于常量,通常用全大写的单词命名,不同的单词间用下划线分隔。
- 初始化: 对于成员变量,有默认初始化机制。如果在实例化之后没有对某些域进行初始化,就会被自动的赋为默认值(数值为0、布尔变量为false、对象引用为null)。 但实际上并不推荐这么做,良好的代码风格会显式的给所有域附上初始值,以增加代码的可读性。
public class Person { private String name; // name = null; private int age; // age = 0; private boolean isBoy; // isBoy = false; }
- 生命周期: 成员变量实例化之后成为对象的实例域,它生命周期与对象的生命周期相同,直到对象被垃圾回收机制彻底回收才会被销毁。
3.2 类变量(静态变量)
类变量,顾名思义,是属于类的变量,它在内存中只有一个,Java虚拟机在加载类的过程中为类变量分配内存。静态变量通常使用的较少,但静态常量却使用的比较多。
静态变量位于方法区,被类的所有实例共享,可以通过类名直接访问,也可以通过对象实例进行访问。
- 变量的定义: 类变量同样定义在类的内部,但需要在它前面加上static关键字。
- 修饰词: 与成员变量相似,在类变量前面同样可以加上访问权限修饰词和final关键字,规则与成员变量相同。
- 初始化: 类变量同样拥有默认初始化机制,初始化规则与成员变量相同。但同样的,即使我们就是想让类变量为默认值,也应该将它显式的表达出来,以增加可读性。
- 生命周期: 类变量的生命周期取决于类的生命周期,直到类被垃圾回收机制回收之后才会被销毁。
3.3 局部变量
- 变量的定义: 局部变量定义在类内部的语句块中,如构造函数、方法等。
- 修饰词: 可以使用final关键字,但不能有访问权限修饰词(毕竟局部变量只在其所在的语句块起作用)。
- 初始化: 局部变量没有默认初始化机制,需要在程序中完成初始化。
- 生命周期: 局部变量的作用域在定义局部变量的语句块中,在语句块结束后就会被销毁。
4. 构造器
- 构造器需要与类同名。
- 每个类可以有一个以上的构造器。(如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。)
- 构造器可以有0个、1个或多个参数。
- 构造器没有返回值。
- 构造器总是伴随着new操作一起调用。
有关构造器的更多内容会在中给出。
5. 方法
在Java中,所有的方法都必须在类内部定义,程序总是会从main()方法开始执行,但main()方法不是所有类都必须的。
类内部定义的方法可以分为实例方法和静态方法(static),mian()方法就是个静态方法。
可以将方法理解成C语言中的函数,它可以实现某种功能,可以有返回值,也可以有方法参数(Java总是采用)。
对于类中的方法,需要注意一点:
类中可以有多个名字相同的方法(方法名相同,但不是同一个方法),前提是它们的参数类型不同。 注意只能是参数不同时,Java中不允许存在仅有返回值不同的同名方法。
public class Person { // ok public void set(String name) { ... } public void set(String name, int age) { ... } // error(不允许存在仅有返回值不同的同名方法,注意仅有两个字) public String get() { ... } public int get() { ... } // ok public String get(int a) { ... } public int get(String str) { ... } }
5.1.1 实例方法
- Java的实例方法可以访问类内部的成员变量和静态变量。
- 在外部调用实例方法时只能通过对象进行调用。
5.1.2 静态方法(static)
如果一个方法被static关键字修饰,那它就是个静态方法。
- 静态方法只能访问类内部的静态变量。
- 在外部调用静态方法时,可以通过类名进行调用,也可以通过对象进行调用。
6. 对象构造
6.1 重载
有些类有多个构造器,我们可以用不同的构造器构造对象,如:
StringBuilder messages = new StringBuilder(); StringBuilder todoList = new StringBuilder("To do: ");
这种特征叫做重载(overloading)。如果多个方法(比如, StringBuilder 构造器方法)有相同的名字、不同的参数,便产生了重载。编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与方法调用所使用的值类型进行匹配来挑选出相应的方法。如果编译器找不到匹配的参数,就会产生编译时错误,因为根本不存在匹配,或者没有一个比其他的更好。这个过程被称为重载解析(overloading resolution)。
当然,Java允许重载任何方法。而不只是构造器方法。
6.2 默认域初始化
如果在构造器中没有显式的给域赋予初值,那么就会被自动地赋为默认值:数值为0、 布尔值为 false、对象引用为null(建议总是显示的给域赋值)。
6.3 关键字this
编写带参数的构造器时,经常会用能够代表变量意义的单词对其进行命名,这就导致了构造器参数经常会和成员变量同名。
如果构造器参数经常会和成员变量同名,那在构造器中使用这个变量名将引用参数,而不是实例域。
这时候该怎么用构造器参数给实例域初始化呢?利用this关键字:
public Person(String name) { this.name = name; }
关键字this可以引用方法的隐式参数,这点对于构造器之外的方法也有效:
public void setName(String name) { this.name = name; }
6.4 调用另一个构造器
关键字this除了可以引用方法的隐式参数,还可以调用另一个构造器(需注意必须在构造器的第一个语句进行调用):
public class ArrayStack { private static final int CAPACITY = 1024; private int capacity; private Object[] objArray; public ArrayStack() { this(CAPACITY); // 调用另一个构造器 } public ArrayStack(int capacity) { this.capacity = capacity; objArray = new Object[capacity]; } }
如果构造器的第一个语句形如 this(...),这个构造器将调用同一个类的另一个构造器。
6.5 关键字super
利用关键字this,可以引用本类的实例域和方法;而关键字super则可以对父类进行引用。
- 如果父类的实例域或方法允许被子类访问,那么我们就可以用super关键字访问它。
public class Person { protected String name = ""; @Override public String toString() { return name; } }
public class Student extends Person { private int studentNumber = 0; @Override public String toString() { return super.name + studentNumber; // or return super.toString() + studentNumber; } }
- 关键字super在构造函数中同样适用
public class Person { private String name; public Person(String name) { this.name = name; } }
public class Student extends Person { private int studentNumber; public Student(String name, int studentNumber) { super(name); // 必须是构造器中的第一个语句 this.studentNumber = studentNumber; } }
同样的,调用父类的构造器方法,也必须是子类构造器的第一个语句。
7. 初始化块
在前面的例子中,已经涉及到了两种初始化数据域的方法:
- 在声明中直接赋值:
private String name = "";
- 在构造器中完成初始化:
private String name; public Person(String name) { this.name = name; }
除此之外,Java还有第三种机制完成数据域的初始化——初始化块(initializationblock)
在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。例如:
public class Person { private String name; private int age; // object initialization block { name = ""; } public Person() { // ... } public Person(String name) { // ... } public Person(String name, int age) { // ... } }
在这个示例中,无论使用哪个构造器构造对象,name域都在对象初始化块中被初始化。首先运行初始化块,然后才运行构造器的主体部分。
这种机制不是必需的,也不常见,只有在特定的场合下可能会用到。