Java_面向对象

本文详细介绍了Java语言的面向对象特性,包括类和对象的定义、构造器、方法的使用、成员变量和局部变量的区别、封装、继承和多态的概念。讲解了方法的参数传递、重载、初始化块以及构造器的执行过程。此外,还探讨了访问控制符的作用,以及如何通过package、import导入类。最后,文章阐述了类的继承特点、重写父类方法的规则,以及多态性的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


Java是面向对象程序设计语言,java语言提供了定义类、成员变量、方法等基本的功能。类可被认为是一种自定义的数据类型,可以使用类来定义变量,所有用类来定义的变量都是引用变量,它们将会引用到类的对象。类用于描述客观世界里某一类对象的共同特征,而对象则是类的具体存在,java程序使用类的构造器来创建该类的对象。 Java也支持面向对象的三大特征:封装、继承和多态,java提供private、protected和public三个访问控制修饰符来实现良好的封装,提供extends关键字来让子类继承父类,子类继承父类可以继承父类的成员变量和方法,如果访问控制允许,子类对象可以直接调用父类里定义的方法。继承和组合具有相同的功能。使用继承关系来实现复用时,子类对象可以直接赋给父类变量,这个变量具有多态性,编程更加灵活;而利用组合关系来实现服用时,则不具备这种灵活性。 构造器用于对类实例进行初始化操作,构造器支持重载,如果多个重载的构造器里包含了相同的初始化代码,则可以把这些初始化代码放置在普通初始化里完成,初始化块总在构造器执行之前被调用。除此之外,java还提供了一种静态初始化块,静态初始化块用于初始化类,在类初始化阶段被执行,如果继承树里的某一个类需要被初始化时,系统将会同时初始化该类的所有父类。

5.1类和对象


java是面向对象程序设计语言,类是面向对象的重要内容,可以把类当成一种自定的类型可以使用类来定义变量,这种类型的变量统称为引用变量。也就是说,所有类是引用类型。
面向对象的程序设计过程中有两个重要概念:类(class)和对象(object,也被称为实例,instance),其中类是某一批对象的抽象,可以把类理解成某种概念;对象才是一个具体存在的实体。

5.1.1 定义类

java语言里定义类的简单语法
{修饰符}class 类名{
零个多个构造器定义
零个多个成员变量
零个多个方法

note:
构造器是一个类创建对象的根本途径,如果一个没有构造器,这个类通常无法创建实例,因此Java语言提供了一个功能;如果程序员没有为一个类编写构造器,则系统会为该类提供一个默认的构造器。一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器。

含有默认构造器的代码

在这public class Person {
	String name;
	int age;
}
public class PersonTest {
	public static void main(String[]args) {
		Person p=new Person();
		System.out.println(p.age );
	}
}
//自定义的构造器
public class Person {
	String name;
	int age;
public Person(String name,int age) {
	this.age=age;
	this.name=name;
}
}`

public class PersonTest {
	public static void main(String[]args) {
		Person p=new Person("ad",2);
		System.out.println(p.age );
	}

}

5.1.2 对象的产生和使用

创建对象的根本途径是构造器,通过new关键字类调用某个类的构造器即可创建这个类的实例。

//使用Person类定义一个Person类型的变量
Person p;
//通过new关键字调用Person类的构造器,返回一个Person实例
//将该Person实例赋给p变量
p=new Person();
//上面代码也可简写成如下形式
//定义p变量的同时并为p变量赋值
//Person p=new Person();
java
在这里插入代码片

创建对象后大致有如下作用。
1️⃣访问该对象的实例变量
2️⃣调用对象的方法
note
如果访问权限允许,类里定义的方法和成员变量都可以通过类或实例来调用。类或实例访问或成员变量的语法是:类.类名|方法,在这种方式中,类或实例是主调者,用于访问该类或该实例的成员变量或方法。
static修饰符的方法和变量,即可通过类来调用,也可通过实例来调用;没有使用static修饰的普通方法和成员变量,只可通过实例来调用。

在这里插入代码片
//访问p的name实例变量,直接为该变量赋值
p.name="李刚"//调用p的say()方法,声明say()方法时定义了一个形参
//调用该方法必须为星璀璨指定一个值
p.say("java语言很简单,学习很容易")//直接输出p的name实例变量,将输出李刚
System.out.println(p.name);

5.1.3 对象、引用和指针

在前面PersonTest.java代码中,有这样一行代码:Person p=new Person();
这行代码创建了一个Person实例,也被称为Person 对象,这个Person对象被赋给P变量。
这行代码中实际产生了两个东西:一个是p变量,一个是Person对象
从Person类定义来看,Person对象应包含两个实例变量,而变量是需要内存存储的,因此,当创建Person对象时,必然需要有对应的内存来存储Person对象的实例变量.
在这里插入图片描述
类也是一种引用数据类型,因此程序中定义的Person类型的变量实际上是一个引用,它被存方法在栈内存里,指向实际的Person对象;而真正的Person对象则存放在堆内存中,下面显示引用变量的示意图。
在这里插入图片描述

栈内存里的引用变量并未真正存储对象的成员变量,对象的成员变量数据实际存放在堆内存里;
而引用变量只是指向该堆内存里对象。从这个角度来看,引用变量与C语言里的指针很像,他们都是存储一个地址值,通过这个地址来引用到实际对象,实际上java里面的引用就是C里的指针,只是java语言把这个指针封装起来,避免开发者进行繁琐的指针操作。
当一个对象被创建成功以后,这个对象将保存在堆内存中,java程序不允许直接访问堆内存中的对象,只能通过该对象的引用操作该对象。也就是说,不管是数组还是对象,都只能通过引用来访问它们。

5.1.4对象的this引用

this关键字总是指向调用该方法的对象。this出现的位置不同作为对象的默认引用有两种情形。
1️⃣构造器中饮用该构造器正在初始化的对象
2️⃣在方法中引用调用该方法的对象

this关键字最大的作用就是让一个方法,访问该类里的另一个方法或实例变量,假设定义了一个Dog类,这个Dog对象的run()方法需要调用它的jump()方法

在这里插入代码片
public class Dog{
int age;
//无参构造
public Dog(){
}
//有参构造
public Dog(int age){
this.age=age;
}
public void jum(){
System.out.println("jump 方法正在执行");
}
public void run(){
this.jump();
System.out.println("run 方法正在执行");
}
}

public class DogTest{
public static void main(String[]args){

Dog d=new Dog("djh");
d.run();
}}

note
1.this关键字只能在本类或继承的子类中使用
2.静态方法不能直接访问非静态方法,所以静态方法中不能使用this关键字。

原因:对于撒台词修饰的方法而言,则可以使用类来直接调用该方法,如果在static修饰的方法中使用this关键字,则这个关键字就无法指向合适的对象,所以static修饰的方法中不能使用this引用。由于static修饰的方法不能使用this引用,所以static修饰的方法不能访问不适用static修饰的普通成员,因此java语法规定:静态成员不能直接访问非静态成员。

//示范代码

在这里插入代码片
public class StaticAccessNonStatic{
public void info(){
System.out.println("简单info方法");public static void main(String[]args){
//因为main()方法时静态方法,而info()时非静态方法
//调用main()方法的是该类本身,而不是该类的实例
//因此省略的this无法指向有效的对象
info();
}
}

2️⃣在方法中引用调用该方法的对象情况
1.成员变量和局部变量同名时使用this关键字成员变量会覆盖掉局部变量
//示范代码

public class ThisInContructor{
//定义一个名为foo的成员变量
public int foo;
public ThisInContructor(){
//构造器里定义一个foo变量
int foo=0;
//使用this代表构造器正在初始化的对象
//下面的代码将会把构造器正在初始化的对象 foo成员变量设为
this.foo=6;
}
public static void main(String[]args){
//所有使用ThisInContructor创建的对象的foo成员变量
System.out.println(new ThisContructor().foo);
}

2.当this 作为对象的默认引用时,程序可以像访问普通引用变量一样来访问这个this引用,甚至可以把this当成普通方法的返回值。
//示范代码

public class ReturnThis{
public int age;
public ReturnThis grow(){
age++;
//return this 返回调用该方法的对象
return this;
}
public static void main(Stringp[]args){
ReturnThis rt=new ReturnThis();
rt.grow().grow();
System.out.println("rt的age成员变量值是:"+rt.age);
}
}

5.2方法详解

方法时类或对象的行为特征的抽象,方法时类或对象最重要的组成部分。但从功能上看方法完全类似于传统结构化程序里的函数,值得指出的是,java里的方法不能独立存在,所有的方法都必须定义在类里。方法在逻辑上要么属于类,要么属于对象。

5.2.1 方法的所属性

不论是从定义方法的语法来看,还是从方法的功能来看,方法函数之间有相似性。
在结构化编程语言里函数是一等公民。整个软件由一个个函数组成;
在面向对象语言中类才是一等公民,整个系统由一个个的类组成。因此在java语言里方法不能独立存在,方法必须属于类或 对象。
因此,方法只能定义在类体内定义,如果这个方法使用了static修饰则这个方法属于这个类,否则这个方法属于这个类的实例。
执行方法时必须使用类或对象 作为调用者,即所有方法都必须使用“类.方法”或“对象.方法”的形式来调用。这里可能产生一个问题:同一个类里不同方法之间相互调用时,不就可以直接调用码?这里需要指出:
同一个类的一个方法调用另外一个方法时,如果被调方法是普通方法,则默认使用this作为调用者;
如果被调用方法是静态方法,则默认使用类作为调用者。,也就是说,表面上看起来某些方法可以被独立执行。

属性主要体现在如下几个方面:
①方法不能独立定义,方法只能在类体里定义
②从逻辑意义上来看,方法要么属于该类本身,要么属于该类的一个对象
③永远不能独立执行方法,执行方法必须使用类或对象作为调用者

5.2.2 方法的参数传递机制

java里方法的参数传递方式只有一种:值传递
所谓的值传递,就是讲实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响

//下面程序演示了方法参数传递的效果
//基本类型的参数传递

在这里插入代码片
public class PrimitiveTransferTest{
public static void main(String[]args){
//下面三行代码实现a、b变量的值交换
//定义一个临时变量来保存a变量的值
int tmp=a;
//把b的值赋给a
a=b;
//把临时变量tmp的值赋给a
b=tmp;
System.out.println("swap方法里么,a的值是"+a+":b的值是"+b);public static void main(String []args){
int a=6;
int b=9;
swap(a,b);
System.out.println("交换后结束,变量a的值是"+a+":变量b的值"+b);
}
}

结果:swap方法里,a成员变量的值是9;b成员变量的值是6;
交换结束后,a成员变量的值是6;b成员变量的值是9;

//下面是引用类型的参数传递的效果

class DataWrap{
int a;
int b;
}

public class ReferenceTransferTest{
public static void swap(String[]args){
//下面三行代码实现dw的a、b两个成员变量的值交换
//定义一个临时变量来保存dw对象的a成员变量的值
int tmp=dw.a;
//把dw对象的成员变量的值赋给a成员变量
dw.a=dw.b;
//把临时变量tmp的值赋给dw对象的b成员变量
dw.b=tmp;
System.out.println("swap方法里,a成员变量的值是"+dw.a+":b成员变量的值是"+dw.b);
}
public static void main(String[]args){
DataWrap dw=new DataWrap();
dw.a=6;
dw.b=9;
swap(dw);
System.out.println("交换结束后,a成员变量的值是"+dw.a+":b成员变量的值是"+dw.b);
}
}

结果:swap方法里,a成员变量的值是9;b成员变量的值是6;
交换结束后,a成员变量的值是9;b成员变量的值是6;

5.2.3形参个数可变的方法

 从JDK1.5之后,java允许定义形参个数可变的参数,从而允许为方法指定数量不确定的形参,如果在定义方法时,在最后一个形参的类型后增加三点(...),则表明该形参可以接受多少个参数值,多个参数值被当成数组传入,下面程序定义了一个形参个数可变的方法。
 //实例代码
在这里插入代码片
public class Varargs{
public static void test(int a,String...args){
//books被当成数组处理
for(String tmp:books)
{
System.out.println(tmp);
}
//输出整数变量a的值
System.out.println(a);
}
public static void main(String[]args){
//调用test方法
test(5,"疯狂学习java","java 面向对象");
}
}

结果:疯狂学习java
java面向对象
5

note:
从上面的运行结果来看出,当调用test()方法时,books参数可以传入多个字符串作为参数值。从test()的方法体代码来看,形参个数可变的参数本质就是一个数组参数,也就是说,下面两个方法签名的效果完全一样

//以可变个数形参来定义方法
public static void test(int a,String...books);
//下面采用数组形参来定义方法
public static void test(int a,String[]args);

区别是什么? 这两种形式都包含了一个名为books的形参,在两个方法的方法体内都可以把books当成数组处理。
但区别是调用两个方法时存在差别,对于以可变形参的形式定义的方法,调用方法时更加简洁,如下面代码所示。

test{5,"疯狂学习","冲击年薪百万");

传给books参数的实参数值无须是一个数组,但如果采用数组形参来声明方法,调用时则必须传给带形参一个数组,如下所示

//调用test()方法时传入一个数组
test(23,new String[]{"疯狂学习""冲击年薪百万");

对比两种调用test()方法的代码,明显第一种形式更加简洁。实际上,即使是采用形参个数可变的形式来定义方法,调用该方法时也一样可以为个数可变的形参传入一个数组。
重点:一个方法中最多只能有一个个数可变的形参。

5.2.4递归方法

一个方法提内调用它自身,被称为方法递归。
方法递归包含了一个隐式的循环它会重复执行某段的代码,但这种重复执行无循环控制。
例题1.
f(0)=1,f(1)=4, f(n+2)=2*f(n+1)+f(n)

public class Recursive{
public static int fn(int n)
{
if (n==0)
{
return 1;
}
else if(n==1){
return 4
}
else
{
//方法中调用它自身,就是方法递归
return 2*fn(n-1)+fn(n-2)
}
}
public static void main(String[]args){
//输出 fn(10)的结果
System.out.println(fn(10));
}}

5.2.5方法的重载

java允许同一个类里定义多个同名方法,只要形参列表不同就行。如果同一个类中包含了两个或两个以上方法的方法名相同,但形参列表不同,则被成为重载

java程序中确定一个方法需要三要素
①调用者:也就是方法的所属这,即可以时类,也可以是对象
②方法名,方法的标识
③形参列表,当调用方法时,系统将会根据传入的实参列表匹配
note: 类名和方法名相同,参数列表不同

//重载示例代码
public class Overload{
//下面定义两个test()方法,但方法的形参列表不同
//系统可以区分这两个方法,这被称为重载
public void test(){
System.out.println("无参数");
}
public void test(String msg){
System.out.println("重载的test方法"+msg);
}
public static void main(String[]args){
Overload o=new Overload();
//调用test()时没有传入参数,因此系统调用上面没有参数的test()方法
o.test();
//调用test()时传入了一个字符串参数
//因此系统用上面带一个字符串参数的test()方法
o.test("hello")
}
}

note:虽然两个test()方法的方法名相同,但因为他们的形参列表不同,所以系统可正常区分出这两个方法。

//当被重载的方法里包含了个数可变的形参,示范代码

public class OverloadVarargs{
public void test(String msg)
{
System.out.println("只有一个字符串参数的test方法");
}
//因为前面已经有一个test()方法,test()方法里有一个字符串参数
//此处的个数可变形参里不包含一个字符串参数的形式
public void test(String...books)
{
System.out.println("*****形参个数可变的test方法****")
}
public static void main(String[]args){
OverloadVarargs olv=new OverloadVarargs();
//下面两次调用将执行第二个test()
olv.test();
olv.test("aa","bb");
//下面调用将长执行第一个test()方法
olv.test("aa");
//下面调用将执行第二个test()方法
olv.test(new String[]{"aa"}); 
}
}

5.3成员变量和局部变量

java语言中,根据定义变量位置的不同,可以将变量分成两大类:成员变量和局部变量
方法内的变量为局部变量
类内方法外的变量为成员变量

public class person{
//定义成员变量
int age;
String name;
char gander;
public void test(){
//定义局部变量
int a;
int b;
String a;
}
}

5.3.1成员变量和局部变量是什么

成员变量:指的是类里面定义的变量
局部变量: 指的是方法里面定义的变量
变量命名规则:第一个单词首字母小写,后面每个单词首字母大写。
成员变量分为:①类变量 ②实例变量。定义成员变量时没有static修饰的就是实例变量,有static修饰的就是类变量。
类变量从该类的准备阶段开始存在,直到系统完全销毁这个类,类变量的作用域与这个类的生存范围相同;
实例变量则从该类的实例被创建开始存在,直到系统完全销毁这个实例,实例变量的作用域与对应实例的生存范围相同。
类变量的访问: 类.类变量
实例变量的访问: 实例.实例变量
**note:**也可以用实例访问类变量→实例.类变量

//示例代码

class Person{
//定义一个实例变量
public String name;
public static int eyeNum;
}

public class PersonTest
{
public static void main(String []args){
//第一次主动使用Person类,该类自动初始化,则eyeNum变量开始起作用,输出0
System.out.println("Person的eyeNum类变量值"+Person.eyeNum);
//创建Person对象
Person p=new Person();
//通过Person对象的引用P来访问Person对象name实例变量
//并通过实例访问eyeNum类变量
System.out.println("p变量的name变量值是:"+p.name+"p对象的eyeNum变量值是:"+p.eyeNum);
//直接为name实例变量赋值
p.name="孙悟空";
//通过p访问eyeNum类变量,依然是访问Person的eyeNum类变量
p.eyeNum=2;
//再次通过Person对象访问name实例变量和eyeNum类变量
System.out.println("p变量的name变量值是:"+p.name+"p对象的eyeNum变量值是:"+p.eyeNum);
//前面通过P修改了Person的eyeNum,此外的Person.eyeNum将输出2
System.out.println("Person的eyeNum类变量值:"+Person.eyeNum);
Person p2=new Person();
//p2访问eyeNum类变量依然引用Person类的,因此依然输出2
System.out.println("p2对象的eyeNum类变量值:"+p2.eyeNum);
}
}

局部变量根据定义形式的不同,又可以被分为如下三种
形参:在定义方法签名时定义的变量,形参的作用域在整个方法内有效
方法局部变量:在方法体内定义的局部变量,这个局部变量的作用域从定义该变量的地方生效,到改代码块结束时失效。
代码块局部变量:在 代码块中定义的局部变量,这个局部变量的作用域从定义该变量的地方生效,到改代码块结束时失效。

**note:**成员变量无须显式初始化,局部变量除形参之外都必须显式初始化。也就是说,必须先给方法局部变量和代码块局部变量指定初始值,否则不可以访问他们。

public class BlockTest
{
public static void main(String[]args)
{
{
//定义一个代码块局部变量
int a;
//下面代码将出现错误,因为a变量还未初始化
//System.out.println("代码块局部变量a的值"+a);
a=5;
System.out.println("代码块局部变量a的值"+a);
}
//下面试图访问a变量并不存在
//System.out.println(a);
}
}

从上面代码块中可以看出,只要离开了代码块局部变量所在的代码块,这个局部变量就立即被销毁变为不可见
对于方法局部变量,其作用域从定义该变量开始,直到方法结束。下面代码示范了方法局部变量的作用域。

public class MethodLocalVariableTest
{
public static void main(String[]args)
{
//定义一个方法局部变量a
int a;
//下面代码将出现错误,因为a变量还未初始化
//System.out.println("方法局部变量a的值:"+a);
//为a变量赋初值,也就是进行初始化
a=5;
System.out.println("方法局部变量a的值:"+a);
}
}

5.3.2成员变量的初始化和内存中的运行机制

当系统加载类偶创建该类的实例时 ,系统自动为成员变量分配内存空间,并在分配内存空间后,自动为成员变量指定初始值。

class Person{
//定义一个实例变量
public String name;
public static int eyeNum;
}

public class PersonTest
{
public static void main(String []args){
//第一次主动使用Person类,该类自动初始化,则eyeNum变量开始起作用,输出0
System.out.println("Person的eyeNum类变量值"+Person.eyeNum);
//创建Person对象
Person p=new Person();
//通过Person对象的引用P来访问Person对象name实例变量
//并通过实例访问eyeNum类变量
System.out.println("p变量的name变量值是:"+p.name+"p对象的eyeNum变量值是:"+p.eyeNum);
//直接为name实例变量赋值
p.name="孙悟空";
//通过p访问eyeNum类变量,依然是访问Person的eyeNum类变量
p.eyeNum=2;
//再次通过Person对象访问name实例变量和eyeNum类变量
System.out.println("p变量的name变量值是:"+p.name+"p对象的eyeNum变量值是:"+p.eyeNum);
//前面通过P修改了Person的eyeNum,此外的Person.eyeNum将输出2
System.out.println("Person的eyeNum类变量值:"+Person.eyeNum);
Person p2=new Person();
//p2访问eyeNum类变量依然引用Person类的,因此依然输出2
System.out.println("p2对象的eyeNum类变量值:"+p2.eyeNum);
}
}
//创建第一个Person对象
Person p1=new Person();
//创建第二个Person对象
Person p2=new Person();
//分别为两个Person对象的name实例变量赋值
p1.name="张三";
p2.name="孙悟空";
//分别为两个Person对象的eyeNum类变量赋值
p1.eyeNum=2;
p2.eyeNum=3;

当程序执行第一行代码 Person p1=new Person(); 时 , 如果这行代码时第一次使用Person类,则系统通常会在第一次使用Person类时加载这个类,并初始化这个类。在类的准备阶段,系统将会为该类的类变量分配内存空间,并指定默认初始值。
当Person类初始化完成后系统内存中的存储示意图。
当Person类初始化完成后,系统将在堆内存中为Person类分配一块内存区(当Person类创建一个类对象),在这块内存区里包含了保存eyeNum类变量的内存,并设置eyeNum的默认初始值:0。

在这里插入图片描述

  系统接着创建了一个Person对象,并把这个Person对象赋给P1变量,Person对象里包含了名为name的实例变量,实例变量是在创建实例时分配内存空间并指定初始值的。当创建第一对象后系统内存中的存储示意图如下

从图可以看出,eyeNum类变量并不属于Person对象,它是属于Person类的,所以创建第一个Person对象时并不需要为eyeNum类变量分配内存,系统只是为name实例变量分配内存空间并指定默认初始值:null
接着执行Person p2=new Person(); 代码创建第二个Person对象,此时因为Person类已经存在于堆内存中了,所以不再需要对Person类进行初始化。创建第二个Person对象与创建第一个Person对象并没有什么不同。

在这里插入图片描述

   当程序执行p1.name="张三";代码时,将为p1的name实例变量赋值,堆内存中的name指向"张三"字符串。执行完成后,两个Person对象在内存中的存储示意图如下
   name实例变量是属于单个Person实例的,因此修改第一个Person对象的name实例变量时仅仅与该对象有关,与Person类和其他Person对象没有任何关系。同样,修改第二个Person对象的name实例变量时,也与Person类和其他Person对象无关。

在这里插入图片描述

直到执行p1.eyeNum=2;代码时,此时通过Person对象来修改Person的类变量,person对象根本没有保存eyeNum这个变量,通过p1访问的eyeNum类变量,其实还是Person类的eyeNum类变量,因此,此时修改的是Person类的eyeNum类变量。修改成功后,内存中显示如下

在这里插入图片描述
当通过p1来访问类变量时,实际上访问的是Person类的eyeNum类变量。
事实上,所有的Person实例访问eyeNum类变量时都将访问到Person类的eyeNum类变量,也就是图中eyeNum,换句话来说,不管通过哪个Person实例访问eyeNum类变量,本质其实还是通过Person类来访问eyeNum类变量时,他们所访问的是同一块内存。

5.3.3局部变量和初始化和内存中的运行机制

    局部变量定义后,必须经过显示初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。

  与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中。如果局部变量是基本类型的变量,则直接把这个变量的保存在该变量对应的内存中;如果局部变量是一个饮用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用对象或数组。
   
     栈内存中的变量无需系统垃圾回收,往往随方法或代码块的运行结束而结束。因此,局部变量的作用域是从初始化该变量开始,直到该方法或该代码块运行完成而结束。因为局部变量只保存基本数据类型的值或者对象的引用,因此局部变量所占的内存去通常比较小。

5.3.4变量的使用规则

对于java初学者而言,何时应该使用类变量?何时应该使用实例变量?何时应该使用方法的局部变量?何时应该使用代码块局部变量?这种选择比较困难,如果仅就程序的运行结果来看,大部分时候都可以直接使用类类变量或实例变量来解决问题,无需使用局部变量。但实际上这种做法相当错误,因为定义一个成员变量时,成员变量将被放置到堆内存中,成员变量的作用域将扩大到类存在范围或者对象存在范围,这种范围的扩大有两个害处。
①增大了变量的生存时间,这将导致更大的内存开销。
②扩大了变量的作用域,这不利于提高程序的内聚性。
对比下面三个程序

public class ScopeTest1{
//定义一个类成员变量作为循环变量
static int i;
public static void main(String[]args)
{
for(i=0;i<10;i++)
{
System.out.println("hello");
   }
  }
}
public class ScopeTest2{
public static void main(String[]args){
//定义一个方法局部变量作为循环变量
int i;
for(i=0;i<10;i++){
  System.out.println("hello");
}
}
}
public class ScopeTest3{
public static void main(String[]args){
//定义一个代码块局部变量作为循环变量
for(int i=0;i<10;i++){
System.out.println("hello");
}
}
}

这三个程序的运行结果完全相同,但程序的效果则大有差异。第三个程序最符合软件开发规范;对于一个循环变量而言,只需要它在循环体内有效,因此只需要把这个变量放在循环体内(也就是在代码块定义),从而保证这个变量的作用域在该代码块内。
※如果如下几种情形,则应该考虑使用成员变量
①如果需要定义的变量是用于描述某个类或某个对象的固有信息的,例如人的身高、体重等信息,它们是人对象的固有信息,这种变量应该定义为成员变量,如果这种信息对这个类的所有实例完全相同,或者说它是相关的,例如人类的眼睛数量,目前所有人的眼睛数量都是2,如果人类进化了,变成了3个眼睛,则所有人的眼睛数量都是3,这种类相关的信息应该定义成类变量;如果这种信息是实例相关的,例如人的身高、体重等,每个人实例的身高、体重可能互不相同,这种信息是实例相关的,因此应该定义成实例变量。
②如果在某个类中需要以一个变量来保存该类或者实例运行时的状态信息,例如上面五子棋程序中的棋盘数组,它用以保存五子棋实例运行时的状态信息。这种用以保存某个类或某个实例状态信息的变量通常应该使用成员变量。
③如果某个信息需要在某个类的多个方法之间进行共享,则这个信息应该使用成员变量来保存。
例如,在把浮点数转换为人民币读法字符串的程序过程中,数字的大写字符和单位字符等是多个方法的共享信息,因此应设置为成员变量。
即使在程序中使用局部变量,也应该尽可能地缩小局部变量的作用范围,局部变量的作用范围越小,它在内存里停留的时间就越短,程序运行性能就越好。因此,能用代码块局部变量的地方法,就坚决不要使用方法局部变量。

5.4隐藏和封装

程序中经常出现通过某个对象的直接访问其成员变量的情形,这可能引起一些潜在的问题,比如讲某个Person的age成员变量直接设为1000,这在语法上没有任何问,但显然违背了现实。因此java程序推荐奖类和对象的成员变量进行封装。

5.4.1理解封装

封装:它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
对一个类或对象实现良好的封装,可以实现以下目的。
①隐藏类的实现细节
②让使用者只能通过实现预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问。
③可进行数据检查,从而有利于保证对象信息的完整性。
④便于修改,提高代码的可维护性。

为了实现良好的封装,需要从两个方面考虑
①将对象的成员变量和实现细节隐藏起来,不允许外部直接访问。
②把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作。

5.4.2使用访问控制符

java提供了3个访问控制符:private、protected和public,分别代表了3个访问控制级别,另外还有一个不加任何访问控制符的访问控制级别(default),提供4个访问控制级别。java的访问控制级别。java的访问控制级别由小到大

private→default→protected→public
default并没有对应的访问控制符,当不使用任何控制符来修饰类或类成员时,系统默认使用访问控制级别。
private(当前类访问权限):如果类里的一个成员(包括成员变量、方法和构造器等)使用private访问控制符来修饰,则这个成员只能在当前类的内部被访问。很显然,这个访问控制用于修饰成员变量最合适,使用它来修饰成员变量隐藏在该类的内部。
default(包访问权限):如果类里的一个成员(包括成员变量、方法和构造器等)或者一个外部类不使用任何访问控制修饰符,就称它是包访问权限的,default访问控制的成员或外部类可以被相同包下的其他类访问。
protected(子类访问权限):如果一个成员(包括成员变量、方法和构造器等)使用protected访问。在通常情况下,如果使用protected来修饰一个方法,通常是希望其子类来重写这个方法。
public(公共访问权限):这是一个最宽松的访问控制级别,如果一个成员(包括成员变量、方法和构造器等)或者一个外部类使用public访问控制符修饰,那么这个成员或外部类就可以按所有类访问,不管访问类和被访问类是否处于同一个包中,是否具有父子继承关系。

在这里插入图片描述
通过上面关于访问控制符的介绍不难发现,访问控制符用于控制一个类的成员是否可以被其他类访问,对于局部变量而言,其作用域就是它所在的方法,不可能被其他类访问,因此不能使用访问控制符来修饰。
对于外部类而言,它可以使用访问控制符修饰,但外部类只能有两种访问控制级别public默认,外部类不能使用private和protected修饰,因为外部类没有处于任何类的内部,也就没有其所在类的内部,所在类的子类两个范围,因此private和protected访问控制符对外部类没有意义。
外部类可以使用public和包访问控制权限,使用public修饰的外部类可以被所有类使用,如声明变量、创建实例;
不使用任何访问控制符修饰的外部类只能被同一个包中的其他类使用。
//下面通过使用合理的访问控制符来定义一个Person类,这个Person类实现了良好的封装

public class Person{
//使用private修饰成员变量,将这些成员变量隐藏起来
private String name;
private int age;
//提供方法来操作name成员变量
public void setName(String name){
//执行合理性校验,要求用户名必须在2~6位之间
if(name.length()>6||name.length()<2){
System.out.println("您设置的人名不符合要求");
return;elsethis.name=name;}
public String getName(){
return this.name;
}
//提供方法来操作age成员变量
public void setAge(int age){
//执行合理性校验,要求用户年龄必须在0~100之间
if(age>100||age<0){
System.out.println("您设置的年龄不合法");
return;
}
else
{
this.age=age;
}
}
public int getAge(){
return this.age;
}
}
public class PersonTest{
public static void main(String[]args){
Person p=new Person();
//因为age成员变量已被隐藏,所以下面语句将出现编译错误
//p.age=1000;
//下面语句编译不会出现错误,但运行时将提示“您设置的年龄不合法”
//程序不会修改P的age成员变量
P.setAge(1000);
//访问P的age成员变量也必须通过其对应的getter方法
//因为上面从未成功设置P的age成员变量,故此处输出0
System.out.println("未能设置age成员变量时:"+p.getAge());
//成功修改P的age成员变量
p.setAge(30);
//因为上面成功设置了P的age成员变量,故此处输出30
System.out.println("成功设置age成员变量后:"+p.getAge());
//不能直接操作p的name成员变量,只能通过对应的setter方法
//因为“李刚”字符串长度满足2~6,所以可以成功设置
p.setName("李刚");
System.out.println("成功设置name成员变量后"+p.getName());
}
}

java类里实例变量的setter和getter方法有非常重要的意义。如果一个java类的每个实例变量都被使用private修饰,并为每个实例变量都提供public修饰setter和getter方法,那么这个类就是一个符合javaBean规范的类。因此,javaBean总是一个封装良好的类。
关于访问控制符的使用,存在如下几条基本原则
①类里的绝大部分成员变量都应该使用private修饰,只有一些static修饰的,类似全局变量的成员变量,才能考虑使用public修饰。除此之外,有些方法只用于辅助实现该类的其他方法,这些方法被称为工具方法,工具方法也应该使用private修饰。
②如果某个类主要用做其他类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不想被外界直接调用,则应该使用protected修饰这些方法。
③希望暴露出来给其他类自由调用的方法应该使用public修饰。因此,类的构造器通过使用public修饰,从而允许在器他地方创建该类的实例。因为外部类通常都希望被其他类自由使用,所以大部分外部类都使用public修饰。

5.4.3pakage、import和import static

pakage
java允许将一组功能相关的类放在同一个package下,从而组成逻辑上的类库单元。如果希望把一个类放在指定的包结构下,应该放在java源程序的第一个非注释行放置如下格式代码:
package packageName;
一旦在java源文件中使用了这个pakage语句,就意味着该源文件里定义的所有类属于这个包。
位于包中的每个类的完整类名都应该是包名和类名的组合,如果其他人需要使用该包下的类,也应该使用包名加类名的组合。
下面程序在lee包下定义了一个简单的Java类

package lee
public class Hello{
public static void main(String[]args){
System.out.println("Hello World");
}
} 

****note:在实际企业开发中,还会在org.crazyit包下以项目名建立子包;如果该项目是足够大,则还会在项目名子包下以模块名来建立模块子包;如果该模块下还包括多种类型的组件,则还会建立对应的子包,。假设有一个elearning系统,对于该系统下学生模块的DAO组件,则通常会放在org.crazyit.elearning.student.dao包下,其中elearning是项目名;student是模块名,dao用于组织一类组件。

import
java引入import 关键字,import可以向某个java文件中导入指定包层次下某个类或全部类,import语句应该出现在package语句(如果有的话)之后,类定义之前,一个java语句用于导入多个包层次下的类,
使用import语句导入单个类的用法如下

import package.subpakage...ClassName;

上面语句用于直接导入指定java类。例如导入前面提到的lee.sub.Apple类,应该使用下面的代码:

import lee.sub.Apple;

使用import语句导入指定包下全部类的使用方法:

import package.subpackage...*;

note:上面import语句中的星号()只能代表类,不能代表包。因此使用import lee. ;语句时。它表明导入lee包下的所有类,

import static 的用法
JDK 1.5以后更是增加了一种静态导入的语法,它用于导入指定类的某个静态成员变量、方法或全部的静态成员变量、方法。
静态导入使用import static 语句,静态导入也有两种语法,分别用于导入指定类的单个静态成员变量、方法和全部静态成员变量、方法,其中导入指定类的单个静态成员变量、方法的语法格式如下

import static package.subpackage...ClassName.filedName|methodName;

上面语法导入 package.subpackage…ClassName类中名为fieldName的静态成员变量或者名为methodName的静态方法。例如,可以使用import static java.lang.System.out;语句来导入java.lang.System类的out静态成员变量。
导入指定类的全部静态成员变量、方法的语法格式如下;
import static package.subpackage…ClassName.*;
上面语法中的星号只能代表静态成员变量或方法名。
import static 语句也放在java源文件的package语句之后,类定义之前,即放在与普通import语句相同的位置,而且import语句和import static语句之间没有任何顺序要求。

//下面程序使用import static 语句来导入java.lang.System类下的全部静态成员变量,从而可以将程序简化成如下形式:

import static java.lang.System.*;
import static java.lang.Math.*;
public class StaticImportTest{
public static void main(String[]args){
//out是java.lang.System类的静态成员变量,代表标准输出
//PI是java.lang.Math类的静态成员变量,表示π常亮
out.println(PI);
//直接调用Math类的sqrt静态方法
out.println(sqrt(256));
}
}

现在可以总结出java源文件的大体结构如下:

package 语句                                   //0个或1个,必须放在文件开始
import |import static 语句                     //0个或多个,必须放在文件开始
public classDefinition |interfaceDefinition |enumDefinition
                                              //0个或1个public类、接口或枚举定义
classDefinition |interfaceDefinition |enumDefinition  
                                              //0个或多个普通类、接口或枚举定义

5.4.4java常用包

java 的核心类都放在java包以及子包下,Java扩展的许多类都放在javax包以及其子包下。这些实用类也就是前面所说的API(应用程序接口)。
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程序

5.5深入构造器

构造器是一个特殊的方法,这个特殊的方法用于创建实例时执行初始化。构造器时创建对象的重要途径(即使使用工厂模式、反射等方法创建对象,其实质依然是依赖于构造器),因此java类必须包含一个或一个以上的构造器。

5.5.1深入构造器执行初始化

构造器最大的用处是在创建对象时执行初始化。前面已经介绍过了,当创建一个对象时,系统为这个对象的实例变量进行默认初始化,这种默认的初始化把所有基本类型的实例变量设为0(对数值型实例变量)或false(对布尔型实例变量),把所有引用类型的实例变量设为null。
如果想改变这种默认的初始化,想让系统创建对象时就为该对象的实例变量显式指定初始值,就可以通过构造器来实现。

//下面是自定义的初始化操作

public class ConstructorTest{
public String name;
public int count;
//提供自定义的构造器,该构造器包含两个参数
public ConstructorTest(String name,int count){
//构造器里的this代表它进行初始化的对象
//下面两行代码将传入的2个参数赋给this代表对象的name和count实例变量
this.name=name;
this.count=count;
}
public static void main(String[]args){
//使用自定义的构造器来创建对象
//系统将会对该对象执行自定义的初始化
ConstructorTest tc=new ConstructorTest("学习java",90000);
//输出ContructorTest 对象的name和count两个实例变量
System.out.println(tc.name);
System.out.println(tc.count);
}
}

因为构造器主要用于被其他方法调用,用以返回该类的实例,因而通常把构造器设置成public访问权限,从而允许系统中任何位置的类来创建该类的对象,除非在一些极端的情况下,业务需要限制创建该类的对象,可以把构造器设置成其他访问权限,例如设置为protected,主要用于被其子类调用,把其设置为private,阻止其他类创建该类的实例。

5.5.2构造器重构

同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载。构造器重载允许java类里包含多个初始化逻辑,从而允许使用不同的构造器来初始化java对象。
//示范构造器重载

public class ConstructorOverload{
public String name;
public int count;
//提供无参数的构造器
public ConstructorOverload(){}
//提供两个参数的构造器
//对该构造器返回的对象执行初始化
public ConstructorOverload(String name,int count){
this.name=name;
this.count=count;
}
public static void main(String[]args){
//通过无参数构造器创建ConstructorOverload对象
ConstructorOverload oc1=new ConstructorOverload();
//通过有参数的构造器创建ConstructorOverload对象
ConstructorOverload oc2=new ConstructorOverload("轻量级JAVA EE企业应用实战",30000);
System.out.println(oc1.name+""+oc1.count);
System.out.println(oc2.name+""+oc2.count);
}
}

上面的ConstructorOverload类提供了两个重载的构造器,两个构造器的名字相同,但形参列表不同,系统通过new调用构造器时,系统根据传入的实参列表来决定调用哪个构造器。

如果系统中包含了多个构造器,其中一个构造器的执行体里完全包含另一个构造器的执行体。

public class Apple{
public String name;
public String color;
public double weight;
public Apple(){}
//两个参数的构造器
public Apple(String name,String color){
this.name=name;
this.color=color;
}
//三个参数的构造器
public Apple(String name,String color,double weight){
//通过this调用另一个重载的构造器的初始化代码
this(name,color);
//下面this引用该构造器正在初始化的Java对象
this.weight=weight;
}
}
 

上面的Apple类里包含了三个构造器,其中第三个构造器通过this来调用另一个重载构造器的初始化代码。程序中this(name,color);调用表明调用该类另一个带两个字符串参数的构造器。
使用this调用另一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句。使用this调用重载的构造器时,系统会根据this后括号里的实参来调用形参列表之对应的构造器。

5.6类的继承

继承是面向对象的三大特征之一,也是实现软件复用的重要手段。java的继承具有单继承的特点,每个子类只有一个直接父类。

5.6.1继承的特点

java的继承通过extends关键字来实现,实现继承的类称为子类,被继承的类称为父类,有的也称其为基类、超类。父类和子类的关系,是一种一般和特殊的关系。例如水果和苹果的关系苹果继承了水果,苹果是水果的子类,则苹果是一种特殊的水果。
java里子类继承父类的语法格式如下:
修饰符 class subClass extends SuperClass
{
//类定义部分

//下面的程序示范了子类继承父类的特点。

public class Fruit{
public double weight;
public void info(){
System.out.println("我是一个水果!重"+weight+"g!");
}
}

public class Apple extends Fruit
{
public static void main(String[]args){
//创建Apple对象
Apple a=new Apple();
//Apple对象本身没有weight成员变量
//因为Apple的父类有weight成员变量,也可以访问Apple对象的weight成员变量
a.weight=56;
//调用Apple对象的info()方法
a.info();
}
}

很多书在介绍Java的单继承时,可能会说java类只有一个父类,严格来讲这种说法是错误的。应该换成如下说法:java类只有一个直接父类,实际上,java类可以无限多个间接父类。例如:
class Fruit extends Plant{…}
class Apple extends Fruit{…}
上面的类定义中Fruit是Apple类的父类,Plant类也是Apple类的父类。区别是Fruit是Apple的直接父类,而Plant则是Apple类的间接父类。
如果定义一个java类时并未显式指定这个类的直接父类,则这个默认扩展java.lang.Object类。
因此,java.lang.Object类是所有类的父类,要么是其直接父类,要么是其间接父类。因此所有的java对象都可调用java.lang.Object类所定义的实例方法。

5.6.2重写父类的方法

子类扩展了父类,子类是一个特殊的父类。大部分时候,子类总是以父类为基础,额外增加新的成员变量和方法,但有一种情况例外:子类需要重写父类的方法。

public class Bird{
//Bird类的fly()方法
public void fly()
{
System.out.println("我在天空里自由自在地飞翔...");
}
}
//下面再定义一个Ostrich类,这个类扩展了Bird类,重写Bird类的fly()方法。

public class Ostrich extends Bird{
//重写Bird类的fly()方法
public void fly(){
System.out.println("我只能在地上奔跑。。。");
}
public static void main(String[]args){
//创建Ostrich对象
Ostrich os=new Ostrich();
//执行Ostrich对象的fly()方法,将输出“我只能在地上奔跑。。。”
os.fly();
}
}

执行上面程序,将看到执行os.fly()时执行的不再是Bird类的fly()方法,而是执行Ostrich类的fly()方法。
这种子类包含于父类同名方法 的现象被称为重写(Override),也成为方法的覆盖。可以说子类重写了父类的方法,也可以说子类覆盖了父 类的方法。
方法的重写要遵循“两同两小一大”规则,“两同”即方法名相同、形参列表相同;“两小”指的是子类方法返回值类型应比父类方法的返回值类型更小或相等,子类方法声明抛出的异常类应该比父类方法声明抛出的异常类更小或相等;“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等,尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。

//例如下面代码会引发编译错误

class BaseClass{
public static void test(){...}
}
class SubClass extends BaseClass{
public void test(){...}
}

当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。
如果需要在子类方法中调用父类中被覆盖的方法,可以使用super(被覆盖的实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法。
如果父类方法具有private访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。例如下面代码时完全正确的。

class BaseClass{
//test()方法时private 访问权限,子类不可访问该方法
private void test(){...}
}
class SubClass extends BaseClass{
//此处并不是方法重写,所以可以增加static关键字
public static void test(){...}
}

方法重载和方法重写在英语中分别是overload和override,重载和重写的区别是什么?
重载主要发生在同一个类的多个同名方法之间,重写发生在子类和父类的同名方法之间
重载方法名相同参数列表相同,重写是方法名相同,参数列表一样,方法体不同。

5.6.3super限定

如果需要在子类方法中调用父类被覆盖的实例方法,则可使用super限定来调用父类被覆盖的实例方法。为上面的Ostrich类添加一个方法,在这个方法中调用Bird类中被覆盖的fly方法。

public void callOverrideMethod(){
//在子类方法中通过super显式调用父类被覆盖的实例方法
super.fly();
}

5.6.4调用父类构造器

5.7多态

5.7.1多态性

5.7.2引用变量的强制类型转换

5.7.3instanceof运算符

5.8继承和组合

5.8.1使用继承的注意点

5.8.2使用组合实现服用

5.9初始化

5.9.1使用初始化块

5.9.2初始化块和构造器

5.9.3静态初始化块

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值