Java类和对象
面向对象基本概念
对象
我们可以把一切事物称为对象(object),例如一台电脑,一个人,一栋房子等等,同样的敲代码,吃东西,睡觉也可以认为是对象。
因此,一个对象一般有两方面特征:
- 状态(又称为属性):用来描述对象的静态特征(例如人的身高、体重、肤色等等)。
- 行为(又称为方法):用来描述对象的动态特征(例如吃东西、喝水、睡觉、行走等等)。
在Java程序中,一切皆为对象。
类
类可以定义为具有类似状态和行为的对象的集合。如人类共同具有区别于其他动物的明显特征有直立行走,使用工具,思考等等,手机都具有打电话、发短信等功能。
所有的事物都可以归到某一类中,例如人都可以归到人类中,电脑手机可以归到电子设备类中等等。
属于类的某一个具体的对象称为该类的一个实例,例如智能手机是手机类的一个实例。
基于以上分析,我们认为实例和对象是同一概念,后文将不区分两者。
类与对象的关系就是抽象与具体的关系,可以简单比喻为类是一张蓝图(图纸上的房子是抽象的),而对象是依据这个类搭建起来的房子(房子是具体的)。
在Java语言中,类是一种数据结构,要想得到对象,必须先定义类,然后才能创建对象。
消息
对象与对象之间并不是孤立的,对象之间存在一定的联系,这种联系通过消息传递。例如程序员敲代码就是程序员这个对象向电脑这个对象传递消息。
面向对象的特性
封装性
封装性:封装就是把对象的属性和方法结合成一个独立的系统单位,并尽可能的隐藏对象的内部细节。
- 例如一个ATM机就是一个封装体,它封装了ATM的属性和方法。
- 对于一个用户来说,接口部分是可见的,例如存取现金的功能,而实现部分是不可见的,例如用户输入金额后,只用在钱币的接口取钱即可,而ATM机是如何从保险箱取钱,并通过传送带送到出口,验钞,钱币不够了如何处理,发现假钞如何处理等等这些内部实现的细节并不可见。
因此,封装提供对对象的保护,防止用户直接接触对象内部的细节。
继承性
继承是生活中常见的一个方法,某人继承了父母的身高,外貌等等。继承可以分为单继承和多继承。
- 单继承指的是子类只能从一个父类继承(即一个子类只有一个父类)
- 多继承指的是子类可以从多个父类继承(即一个子类可以有超过一个父类)
Java中,只有单继承,即一个子类只能从一个父类继承
多态性
多态性是面向对象编程语言的一个重要的特性,多态可简单理解为同一个名字多种形态。
面向对象中,多态有多种情况:
- 静态多态:在同一个类中定义了多个名称相同的方法,即方法重载(overload)。
- 动态多态:子类定义的与父类同名的方法,即方法重写(覆盖/覆写)(override)。
除此之外,面向对象还有一些其他特性,而上述的三个特性是面向对象的最重要的三大特性。
本文将着重介绍类的定义和对象的创建以及初始化相关知识,关于上述的三大重要特性,将在后边的文章中着重介绍。
类的定义
类声明的一般格式:
[public][abstract|final] class ClassName[extends SuperClass][implements InterfaceName]{
variable declarations;//成员变量
method declarations;//成员方法
}
一个完整的类定义如上所示,包括类的声明和类体(成员变量和成员方法)的定义。
一个普通的类通常按如下方法定义:(以猫类为例)
public Class Cat{
//成员变量
String name;//猫咪的名字
String sex;//猫咪的性别
//其他可以描述猫咪属性的变量
//成员方法
//猫咪吃的行为
public void eat(){
System.out.println(name + "正在吃东西。");
}
//猫的其他一些行为
}
类声明
在类的声明中,[]
中的代码不是必须写的,而是依据实际需要进行添加。
类的访问限定修饰符
限定访问是封装特性的一大体现,它表明了当前类的访问权限,在下文中,我们在着重介绍成员属性限定访问修饰符,它界定了比类更进一步的限定访问。
类的访问限定修饰符为public
或缺省:
public
修饰的类表示公共类,即该类可以被任意包中的类使用。- 若缺省,表示该类只能被同一个包中的类使用。
类的其他修饰符
final
:由单词的意思以可看出,表示最终,即由该修饰符修饰的类为最终类,其不能被继承。abstract
:抽象的,表示该类为抽象类,抽象类不能实例化,即不能由该类创建对象,但抽象类可以被继承,将在介绍多态性时具体介绍抽象类。
class ClassName
类名由实现者来定义,通常是名词,类名除了遵守Java标准中规定的命名规范外,在Java程序猿中,约定俗称类名一律采用大驼峰命名方式(但不是强制的),而方法名和变量名则采用小驼峰命名的方式。
可以不采用上述约定俗称的命名方式,但是这样会大大降低代码的可读性。
extends SuperClass
extends
表示延伸、扩大之意,即ClassName
延伸于SuperClass
,即ClassName
继承了SuperName
。(父类又叫基类、超类,子类又叫派生类)。
例如猫类继承于动物类:
public class Animal{
//类体,略
}
public class Cat extends Animal{
//类体,略
}
关于继承类的具体细节将在讲继承特性中具体详细的介绍。定义类时如果未指明所继承的父类,那么该类自动继承Object
类,即上述代码中,Animal
继承了Object
类,其完整的写法应为public class Animal extends Object
,,换句话说,继承Object
的类可以省略extends
部分,Object
类是Java的根类,因为Java只支持单继承,所以一个类至多只能有一个超类。
implements InterfaceNameList
implements
为接口的关键词,定义了类实现哪个或哪些接口,一个类可以实现多个接口,多个接口之间用逗号隔开。一个类实现多个接口,其目的是为了弥补一个类只能继承一个父类的缺陷。接口的具体细节也在后边介绍。
例如:猫类继承动物类实现吃和跑的接口
//动物类
public class Animal{
//类体
}
//吃的接口
public interface IEatting{
void eat;
}
//跑的接口
public interface IRunning{
void run;
}
//猫类
public class Cat extends Animal implements IEatting,IRunning{
//类体
}
成员的定义
类的声明结束后是一对{}
,{}
括起来的部分即为类体。类体中通常定义两部分类容:
- 成员变量(属性)(variable declarations):提供了类即对象的状态。
- 成员方法(method declarations):实现类和对象的行为。
成员变量的定义
成员变量的声明格式:
[public|protected|private][static][final][volatile] type variableName [= value];
限定访问修饰符
public
:公共变量,可以被任意的类和方法使用。protected
:受保护的变量,可以被自己的子类和同一个包中的类或方法访问。- 空白访问修饰符:即不加任何修饰符,只能被同一个包中的类访问。
prvate
:私有变量,只能在当前类中使用。其他类无法使用,这种类所需的访问权限最高,是最安全的变量,因为只能被当前类使用,不能被外界访问,因此可以实现信息的隐藏,从而保证了我们所讲的封装性。
可以看出,上边的访问修饰符,由上到下所需的访问权限是逐级增高的。
(注:这里的访问权限,举个例子,对于一栋办公大楼,其公共休息区所有人都可以随意进入,所以访问权限最低,而存放机要档案的房间只能最高领导进入,普通人无法访问,所需的访问权限最高)
实例变量和类变量–static
关键字
由static
修饰的变量称为类变量,没有static
修饰的变量称为实例变量。实例变量和类变量的区别是:
- 实例变量是属于一个对象(实例)的,而类变量是属于一个类的。
- 实例变量在其对应的对象中有属于自己的内存空间,类变量相当于多个对象共享一块空间
- 实例变量仅能通过实例来访问,而类变量可以通过类访问,也可以通过实例访问,但实际上,类变量通过实例访问时,编译器首先根据实例找到其对应的类,然后通过类访问的,本质上类变量依旧是通过类访问的。
type variableName
成员变量可以任意已知的Java数据类型,包括用户自定义的类类型,变量的命名通常使用小驼峰命名法(即首单词的首字母小写),成员变量的作用域为整个类,类中所有的方法都可以访问成员变量。
final
关键字
由final
修饰的变量叫最终变量,也为标识符常量。常量可以在声明时赋初值,也可以在后面赋值,常量一旦赋值,其值就不能改变了。
volatile
关键字
由该关键字修饰的成员变量称为共享变量,在多线程程序中,共享变量可以被异步修改。(本文不做讨论)
成员方法的定义
成员方法的定义包括方法的声明和方法体的定义,其格式一般如下:
[public|protected|private][static][final|abstract][native][synchronized] returnType methodName([paramList])[throws ExceptionList]{
method body;
}
限定访问修饰符
成员方法的限定访问修饰符和成员变量的限定访问修饰符含义一样,不再过多介绍。
类方法和实例方法–static
关键字
同类变量和实例变量一样,由static
修饰的方法称为类方法,没有该关键字修饰的称为实例方法,实例方法只能通过实例调用,类方法既可以通过实例调用也可以通过类调用,但通过实例调用的本质上依旧是通过类调用的。
final
和abstract
方法
同成员变量一样,由final
修饰的方法为最终方法,最终方法不能被重写,但是可以重载,方法的重写与继承有关,将在继承部分详细讨论。
由abstract
修饰的方法为抽象方法,如果某个类定义了抽象方法,那么该类必须为抽象类,由于抽象类不能被实例化,所以抽象类必须被继承,继承后抽象方法也必须被重写。
本地方法–native
由native
修饰的方法称为本地方法,本地方法是用来的调用由其他语言编写的函数/方法的。
本地方法是由其他语言(如C、C++ 或其他汇编语言)编写,编译成和处理器相关的代码。本地方法保存在动态连接库中,格式是各个平台专用的,运行中的java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。
同步方法–synchronized
由synchronized
修饰的方法称为同步方法,同步方法主要用于多线程程序。(本文不做讨论)
说明
为了保证封装性,在定义成员变量和方法的时候,尽量使用private
级别的变量或方法,当该种变量或方法不能满足要求时,再逐步降低权限,而public
修饰的成员变量和方法应是最后考虑的,能不使用则尽量不去使用。
对象的创建
和一般的变量一样,对象的定义格式如下:
TypeName objecetName = new TypeName();
例如:创建一个猫的实例:
Cat cat1 = new Cat();
在上述代码中,=
的右边创建了一个对象,然后把地址返回传给了objectName
,即objectName
中存放的是一个地址,该地址指向的对象类型是TypeName
。
对象的初始化
默认初始化
在创建类时,仅声明成员名称,不为其赋值,系统会根据不同的变量为其赋初值。其规则如下:
- 整型(
byte
、int
、short
、long
):0 - 浮点型(
float
、double
):0.0 - 布尔型(
boolean
):false
- 字符型(
char
):\u0000
- 引用类型(例如
String
):null
例如:声明一个猫类,声明成员变量时,不赋初值,输出其值。
public class Cat {
public int age;
public double height;
public char sex;
public String name;
}
创建一个猫的实例,其初值如下:
就地初始化
就地初始化即为在声明类的成员变量的同时为其赋初值。例如:
public class Cat {
public int age = 2;
public double height = 10.5;
public char sex = '母';
public String name = "胖橘";
}
创建猫的实例:
public class Main {
public static void main(String[] args){
Cat cat = new Cat();
System.out.println("猫的年龄:" + cat.age);
System.out.println("猫的体重:" + cat.height);
System.out.println("猫的性别:" + cat.sex);
System.out.println("猫的名字:" + cat.name);
}
}
其默认的初值为:
代码块初始化
有时候,在声明类的时候,也会使用代码块,在创建变量时,使用代码块完成对变量的初始化。
例如:
public class Cat {
public int age;
public double height;
public char sex;
public String name;
{
age = 3;
height = 10.7;
sex = '公';
name = "小花";
}
}
和就地初始化创建的猫咪实例一样,其结果输出为:
static
修饰的代码块
static
修饰的代码块同样也可以用于初始化成员,其具有如下特性:
- 只能初始化类成员,即由
static
修饰的成员,不能初始化对象成员(否则会编译出错)。 static
修饰的代码块只在类加载的时候执行一次,创建对象的时候不执行。- 无论
static
修饰的代码块位于类中的哪个位置,其始终优于普通代码块先执行。 - 类加载始终是在创建实例之前的。
例如:
public class Cat {
public int age;
public double height;
public char sex;
public String name;
public static int eye;
{
System.out.println("这是普通的代码块!");
age = 3;
height = 10.7;
sex = '公';
name = "小花";
}
static {
System.out.println("这是static修饰的代码块");
eye = 2;
}
}
为说明static
修饰的代码块执行的顺序,特地将其顺序放置在普通代码块之后,同时为说明该代码块仅在类加载的时候执行一次,创建对象的时候不执行,创建两个猫的实例:
public class Main {
public static void main(String[] args){
Cat cat1 = new Cat();
System.out.println("猫1的年龄:" + cat1.age);
System.out.println("猫1的体重:" + cat1.height);
System.out.println("猫1的性别:" + cat1.sex);
System.out.println("猫1的名字:" + cat1.name);
Cat cat2 = new Cat();
System.out.println("猫2的年龄:" + cat2.age);
System.out.println("猫2的体重:" + cat2.height);
System.out.println("猫2的性别:" + cat2.sex);
System.out.println("猫2的名字:" + cat2.name);
}
}
程序运行结果为:
可以看到,static
代码块确实只执行了一次,并且优先于普通代码块执行。
在成员的定义–实例变量和类变量一节中说过,类变量可以通过类访问,也可以通过成员访问,但通过成员的访问本质上还是通过类访问的,也可以理解为类变量是多个对象共享同一块空间,此处用上述的例子进行验证:
public class Main {
public static void main(String[] args){
Cat cat1 = new Cat();
Cat cat2 = new Cat();
System.out.println("这是通过Cat类进行访问的eye变量:" + Cat.eye);
System.out.println("这是通过cat1进行访问的eye变量:" + cat1.eye);
System.out.println("这是通过cat2进行访问的eye变量:" + cat2.eye);
Cat.eye = 3;
System.out.println("修改后的eye:");
System.out.println("这是通过Cat类进行访问的eye变量:" + Cat.eye);
System.out.println("这是通过cat1进行访问的eye变量:" + cat1.eye);
System.out.println("这是通过cat2进行访问的eye变量:" + cat2.eye);
}
}
程序的运行结果如下:
可以看出,运行结果符合我们的描述。
构造方法初始化
构造方法是实例初始化的另一种方式,构造方法的方法名和类名一致,其不需要返回值类型,内部也不需要写return
语句,同样的构造方法不需要显示的调用,在创建对象时自动被调用,构造方法和其他的一般方法一样,支持重载。
例如:
public class Cat {
public int age;
public double height;
public char sex;
public String name;
public static int eye;
public Cat(){
age = 10;
height = 6.7;
}
public Cat(String name,int age){
this.name = name;
this.age = age;
}
}
上述的两段代码即为两个构造方法,其中一个没有参数,一个含有参数,对于无参数的构造方法,创建对象时不传入参数,而有参数的构造方法创建对象时必须传入对应类型的参数。
例如:
public class Main {
public static void main(String[] args){
Cat cat1 = new Cat();
Cat cat2 = new Cat("小黑",5);
System.out.println("猫1的年龄:" + cat1.age);
System.out.println("猫1的体重:" + cat1.height);
System.out.println("猫1的性别:" + cat1.sex);
System.out.println("猫1的名字:" + cat1.name);
System.out.println("猫2的年龄:" + cat2.age);
System.out.println("猫2的体重:" + cat2.height);
System.out.println("猫2的性别:" + cat2.sex);
System.out.println("猫2的名字:" + cat2.name);
}
}
程序的运行结果如下:
两个猫咪则按照其对应的构造方法进行初始化。
this
关键字
在上边的含有参数的构造方法中,使用了this
关键字,可以看出,该构造方法中形参的名字与我们上边定义得到成员变量名字一致了,为了以示区分,我们在方法内部使用this
关键字表示是成员变量。
this
关键字的含义:
- 获取到当前对象的引用,即
this
中存放的是当前对象的地址。 - 指向的类型就是当前类的类型。
this
的引用指向是不能修改的(也就是只能指向当前的对象)。this
不能是null
。this
是与对象相关的引用,而static
修饰的代码块或方法是类相关的,所以无法在类方法和static
修饰的代码块中使用。
创建实例后初识化
和普通的变量一样,也可以在创建实例后对其变量进行初始化。
公有成员变量的初始化
对于公有变量的初始化,可直接通过其引用对其初始化,例如:
public class Cat {
public int age;
public double height;
public char sex;
public String name;
public static int eye;
}
创建猫咪实例后为其初始化:
public class Main {
public static void main(String[] args){
Cat cat1 = new Cat();
cat1.age = 3;
cat1.name = "小红";
cat1.height = 3.6;
cat1.sex = '公';
System.out.println("猫1的年龄:" + cat1.age);
System.out.println("猫1的体重:" + cat1.height);
System.out.println("猫1的性别:" + cat1.sex);
System.out.println("猫1的名字:" + cat1.name);
}
}
程序运行的结果为:
私有成员变量的初始化
对于私有变量,即由private
修饰的变量,因为其只能在当前类中使用,无法被其他类使用,所以需要使用方法来对其进行访问修改。例如:
public class Cat {
private int age;
//获取age的信息
public int getAge(){
return age;
}
//修改age的信息
public void setAge(int age) {
this.age = age;
}
}
如上所示,我们可以定义一组方法,创建对象后,通过对象的引用访问这两个方法,用来获取和修改私有成员age的信息,这种方式,我们形象的将其称为“setter
”和“getter
”方法。
使用方式如下:
public class Main {
public static void main(String[] args){
Cat cat1 = new Cat();
System.out.println("获取cat1的age:" + cat1.getAge());
System.out.println("修改age:");
cat1.setAge(5);
System.out.println("修改后cat1的age:" + cat1.getAge());
}
}
程序运行结果如下:
初始化的顺序
在一个类中,大多数时候并不是只有一种初始化方式,而是有多种初始化在一起,当有多种初始化时,他们的执行顺序如下:
首先执行静态代码块(static
)修饰的,无论该代码块位于类中的何处位置,然后执行就地初始化和普通代码块,这两者按照在程序出现的顺序顺次执行,最后执行构造方法里的代码块,同样的无论构造方法代码块位于程序哪里,都在最后执行。
上述初始化执行完毕后,才是手动初始化(即在主程序中通过“setter
”和“getter
”等方式初始化)。例如:
public class Cat {
public Cat(){
System.out.println("这是构造方法的代码块,这段代码位于类的最开头");
}
int a =retInt();
{
System.out.println("这是普通代码块1");
}
int b =retInt1();
{
System.out.println("这是普通代码块2");
}
static {
System.out.println("这是静态代码块,这段代码位于普通代码块和就地初始化代码块的下边");
}
public int retInt(){
System.out.println("这模仿的是就地初始化1");
return 0;
}
public int retInt1(){
System.out.println("这模仿的是就地初始化2");
return 0;
}
}
程序的结果如下,符合我们所说的顺序:
包
随着代码的不断书写,代码逐渐增多,类也逐渐增多,为了便于存储和管理,我们可以将类分门别类的放入不同的文件夹下,而这一个一个文件夹对应的就是包。
导入包的命令如下:
import packageName;
常见的系统包及其功能如下:
java.lang
:包括了系统常用的基础类String
、Object
等,当打开Java的JVM时,此包自动加载。java.lang.reflect
:Java反射编程包java.net
:Java网络编程包java.sql
:数据库开发支持包java.util
:Java提供的工具程序包
toString
方法
toString
方法用于返回反映对象的字符串,该方法是Object
自带的方法,一个类未指定父类,则表示继承Object
类,自然也会继承该类的方法,一个类指定了父类,它的父类继承了该方法,所以它也会从父类继承该方法,但是由系统提供的该方法返回的是对象所在的包、类名以及对应的散列(又称哈希)码。调用cat1
的toString
方法后结果如下:
但通常我们想要得到的是该对象的一些具体信息,例如猫咪对象中,猫咪的名称,性别,年龄等,因此我们需要对该方法进行重写。代码如下:
public class Cat {
public int age;
public double height;
public char sex;
public String name;
public static int eye;
@Override
public String toString() {
return "Cat{" +
"age=" + age +
", height=" + height +
", sex=" + sex +
", name='" + name + '\'' +
'}';
}
}
创建猫咪类,并进行初始化,再次调用该方法:
public class Main {
public static void main(String[] args){
Cat cat1 = new Cat();
cat1.age = 3;
cat1.name = "小红";
cat1.height = 3.6;
cat1.sex = '公';
System.out.println(cat1.toString());
}
}
其结果如下:
总结
以上则是关于面向对象中类和对象创建的一些基本语法和注意事项,后边将重点介绍前述面向对象的三种重要特性。(未完待续……)