java支持面向对象的三大特征:封装、继承、多态。提供了private、protected和public三个访问控制符来实现封装,提供extends关键字支持继承
1 类和对象
1.1 类
类的定义语法如下:
[修饰符] class 类名
{
零到多个构造函数的定义…….
零到多个属性……..
零到多个方法……..
}
类中各成员之间定义的顺序无任何影响,各成员之间可以相互调用,static修饰的成员不能访问没有static修饰的成员。
①修饰符
②构造器
如果程序没有明确写出构造器,系统会默认形成一个构造器,一旦程序员提供了构造器,默认的构造器则不会生成。其语法格式如下:
[修饰符] 构造器名(形参列表)
{
//构造器体
}
修饰符:修饰符可以省略,可以是public、protected、private之一
构造器名称:构造器名必须和类名相同
形参列表:和定义方法形参列表的格式完全相同
需要注意的是:构造器不能定义返回值类型,否则会报错。另外,不能再构造器中显式地使用return来返回当前的对象,因为构造器的返回值是隐式的。
③属性(Field)
定义属性的语法:[修饰符] 属性类型 属性名 [=默认值]
修饰符:修饰符可以省略,可以是public、protected、private、static、final。
④方法
定义方法的语法如下:
[修饰符] 返回值类型 方法名(形参列表)
{
//函数体
}
修饰符:可以省略,也可以是public、protected、private、static、final、final、abstract,其中abstract和final只能出现一个,与static组合起来修饰方法。
static是一个特殊的关键字,它可以用于修饰方法、属性,表明是属于这个类共有的,而不是属于类的单个实例,因此常把static修饰的属性和方法也称为类属性、类方法。
1.2 对象之this
java提供一个this关键字,this关键字是一个对象的默认引用。this关键字总指向调用该方法的对象,this作为对象的默认引用有两种情形:
a)构造器中引用该构造器初始化对象;
b)在方法中引用调用该方法的对象;
this关键字最大的作用是让类中的一个方法,访问该类的另一个方法或属性。this可以代表任何对象,当this出现在某个方法体中时,它所代表的对象是不确定的,但是它的类型是确定的,它所代表的对象只能是当前类。只有当这个方法被调用时,它所代表的对象才能被确定下来:谁调用这个方法,this就代表谁。示例demo如下:
public class Person
{
public int age;
public float hight;
public void run()
{
System.out.print("person is running\n");
}
public void sport()
{
this.run();
System.out.print("person is do sports\n");
}
};
在实际类方法的设计过程中,经常有两种方法之间存在依赖,java允许对象成员直接调用另一个成员,可以省略this前缀,因此上述sport方法改成如下形式也是正确的:
public void sport()
{
run();
System.out.print("person is do sports\n");
}
必须使用this指针的情况:如果方法里面有一个局部变量与方法同名,但程序又需要在该方法里面访问这个被覆盖的属性,则必须使用this前缀。
同理,在构造器中访问其他属性和方法的时候,如果构造器中有一个与属性同名的局部变量,又必须在构造器中访问这个被覆盖的属性,则必须使用this前缀。
1.3 对象之static
对于static修饰的方法而言,则可以使用类来直接调用该方法,static修饰发方法中不能使用this引用,且不能访问不使用static修饰的普通成员(静态成员不能直接访问非静态成员)。
在同一个类中,非static成员方法可以访问static成员方法,static成员方法却不能访问非static方法;在不同类中则可以通过“类名.静态方法”来调用static方法,也可以创建一个新的对象,通过“对象.方法”来调用static方法。
2 方法
方法与函数之间具有相似性。面向过程编程软件由一个一个的函数组成,在面向对象编程中,整个系统由一个一个的类组成。因此在java语言中,方法不能独立存在,方法必须属于类或对象。
①方法的参数传递机制
java方法里的参数传递只有一种:值传递,及传递的是实际参数的一个副本,而参数本身不会受到影响。
a)基本类型的参数传递
示例代码如下:
public class HelloWorld
{
public static void Swap(int a,int b)
{
int temp;
temp = a;
a = b;
b = temp;
}
public static void main(String[] args)
{
int a = 6,b=9;
System.out.print("before swap,a ="+a+";b ="+b+'\n');
Swap(a,b);
System.out.print("after swap,a ="+a+";b ="+b+'\n');
}
}
打印结果如下:
before swap,a =6;b =9
after swap,a =6;b =9
b)引用类型的参数传递
示例代码如下:
public class Person
{
public int a;
public int b;
};
public class HelloWorld
{
public static void swap(Person p)
{
int temp;
temp = p.a;
p.a = p.b;
p.b = temp;
}
public static void main(String[] args)
{
Person dw = new Person();//定义dw引用变量指向Person对象
dw.a = 6;
dw.b = 9;
System.out.print("before swap,a ="+dw.a+";b ="+dw.b+'\n');
swap(dw);
System.out.print("after swap,a ="+dw.a+";b ="+dw.b+'\n');
}
}
打印结果如下:
before swap,a =6;b =9
after swap,a =9;b =6
代码详细分析:main函数开始,定义dw引用变量指向Person对象,这是与基本类型不同的地方!然后通过Dw对象给属性a和b赋值,在内存中的分布情况如下:
接下来main方法开始调用swap()方法,此时系统会为main和swap开辟两个栈区,分别用于存放main和swap方法的局部变量。调用swap方法时,dw作为实参传入swap方法,同样采用值传递方式,将dw变量的值赋给swap方法里的形参p,由于main中的dw是一个引用(也就是一个指针),保存的是对Person对象的地址,因此swap方法的形参p也保存了该地址,存储示意图如下:
通过上面的内存分布示意图就很容易看出为什么得出值发生交换的原因了。
②形参可变的方法
允许方法指定数量不确定的形参,方法是在最后一个参数类型后增加三点(…),表明该形参可以接受多个参数值,多个参数值被当做数组传入,demo如下:
public class HelloWorld
{
public static void test(int num,String ... book)
{
System.out.print("a ="+num +'\n');
for(String tmp:book)
{
System.out.print(tmp+'\n');
}
}
public static void main(String[] args)
{
test(15,"hello","java学习");
}
}
打印结果如下:
a =15 //如果System.out.print(num +'\n');此句输出25,why?
hello
java学习
上述语句改为public static void test(int num,String[] book),结果与上述一样。
二者的区别是:后面传入数组的方法就必须要传递一个数组,调用方式改变成:test(15,new String[]{"hello","java学习"});可见没有第一种方式灵活。
③递归方法
与之前C/C++递归设计的思想一致。
④方法的重载
重载的条件:要求同类、同方法名字,参数列表不同,至于其它部分,如返回值类型、修饰符等,与重载没有任何关系。
需要注意的是带有可变长度形参的重载方法,示例如下:
package OverloadVar;
public class OverloadVar
{
public void test(String msg)
{
System.out.println("固定长度被调用");
}
public void test(String ... msg)
{
System.out.println("可变长度被调用");
}
}
importOverloadVar.OverloadVar;
public class HelloWorld
{
public static void main(String[] args)
{
OverloadVar olv = new OverloadVar();
olv.test(); //可变长度被调用
olv.test("aa"); //固定长度被调用
olv.test("aa","bb"); //可变长度被调用
olv.test(new String[]{}); //可变长度被调用
olv.test(new String[]{"aa"}); //可变长度被调用
olv.test(new String[]{"aa","bb"}); //可变长度被调用
}
}
3 成员变量和局部变量
3.1 成员变量和局部变量的分类
①成员变量:成员变量无须显式地初始化,只要为一个类定义了类属性或实例属性,系统会在类的准备阶段或穿件类的实例时进行默认初始化,成员变量默认初始化的赋值规则与数组动态初始化时数组元素的赋值规则完全相同。
②局部变量:除了形参之外,都必须显示地初始化,否则会报错。
3.2 成员变量和局部变量的区别
关于成员变量和局部变量同名的问题:如果方法里面的局部变量和成员变量同名,则局部变量会覆盖成员变量,如果需要在这个方法里引用被覆盖的成员变量,则可以使用this(对于实例属性)或类名(对于类属性)作为调用者来限定访问成员变量。
成员变量和局部变量存放的地方:成员变量存放在堆内存中,局部变量存放在其所在方法的栈内存中。
3.3 成员变量和局部变量的选用规则
定义成员变量时,成员变量将被放置在堆内存中,其作用域的范围将扩大至类存在的范围或者对象存在的范围,引发以下两个问题:
①增大了变量的生存时间,加大系统的开销。
②扩大作用域的范围,不利于提高程序的内聚性。
使用成员变量的情况总结如下:
①如果需要定义的变量用于描述某个类的固有信息(如Person类的眼睛数量、耳朵数量等,以static修饰)或者某个对象的固有信息(如Person类都含有身高、体重等,不以static修饰),则需要对应地定义为类属性和实例属性。
②如果某个类需要以一个变量来保存该类或实例运行时的状态信息,如五子棋实例,则需要使用成员变量。
③如果某个信息需要在某个类的多个方法之间共享,则这个信息应该使用成员变量来保存。
4 隐藏和封装
封装:是指将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。即隐藏细节,暴露方法。
4.1 使用访问控制符
java提供三个访问控制符:private、protected、public,外加一个默认的访问控制级别default,访问控制级别如下:
| private | default | protected | public |
同一个类中 | √ | √ | √ | √ |
同一个包中 |
| √ | √ | √ |
子类中 |
|
| √ | √ |
全局范围内 |
|
|
| √ |
如果一个java源文件定义所有的类都没有使用public修饰,则这个java源文件的文件名可以是一切合法的文件名。但如果一个java源文件里定义了一个public修饰的类,则这个源文件的文件名必须与public类同名。
访问控制符的使用原则:
①类中除static修饰的、类似全局变量的属性考虑使用public修饰外,其它绝大部分属性都应该使用private修饰。另外一些工具方法(用于辅助实现该类的其他方法)也定义成private。
②如果某个类主要用作其它类的父类,该类里包含的大部分方法可能仅希望被子类重写,而不希望被外界直接调用,则应该使用protected修饰;
③希望暴露给其它类的方法用public修饰,构造器应用public修饰。
4.2 package和import
①package
java允许将一组功能相关的类放在同一个package下,从而组成逻辑上的类库单元,如果希望把一个类放在指定的包结构下,则在java源程序的第一个非注释行放如下格式的代码:
packagepackageName;
那么位于包中的每个类的完整类名都应该是包含包名和类名的组合。
②import
用于向某个java文件中导入指定包层次下某个类或全部类。
静态导入:importstatic package.subpackage…ClassName.fieldName;用于导入指定类的单个静态属性和全部静态属性。
4.3 深入构造器
构造器最大的用处就是在创建对象时执行初始化,当创建一个对象时,系统为这个对象的属性进行默认的初始化,把基本类型设置为0(对数值型属性)或者false(对布尔型属性),把所有引用类型的属性设置为null。程序员未定义,则系统会默认生成一个;另外,构造器可重载。
在一个构造器中调用另一个重载的构造器使用this调用来实现,在子类构造器中调用父类的构造器使用super调用来实现。
4.4 类的继承
继承使用的关键字:extends
①重写或覆盖(override)----à发生在父类和子类之间
子类与父类具有同名的方法称为方法重写,也称为方法覆盖(override)。
方法的重写要遵循“两同两小一大”的原则。“两同”是指方法名相同、形参列表相同;“两小”是指子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类型更小或相等;“一大”是指子类方法的访问权限应该比父类方法更大或相等。
子类覆盖父类方法后,子类对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法:使用super(被覆盖的是实例方法)或父类名(被覆盖的是类方法)。
另外,如果父类方法是private访问权限,子类则无法访问,在子类中定义定义一个与父类private方法同名的方法不属于重写。
super与static一样不能出现在static方法中。另外,在构造器中使用super引用,则super引用指向该构造器正在初始化的对象所对应的父类对象。
②重载(overload或override)------à发生在同一个类但是同名不同参数的方法之间
4.5 多态
4.5.1 多态性
java引用变量有两个类型:编译时的类型(由申明该变量时使用的类型决定) 和 运行时的类型(实际赋给该变量的对象决定)。如果编译时类型和运行时类型不一致,就出现所谓的多态(Polymorphism)。
class Base
{
void test()
{
System.out.println("Base test called!");
}
};
public class Deriveextends Base
{
void test()
{
System.out.println("Derive test called!");
}
public static void main(String[] args)
{
Base bs = new Base();
bs.test(); //输出Basetest called!
Derive cs = new Derive();
cs.test(); //输出Derivetest called!
Base bs1 = new Derive();
bs1.test(); //输出Derive test called!
}
}
bs1编译时类型为Base,运行时是Derive,这就是多态。
4.5.2 引用变量的强制类型转换
编写java程序时,引用变量只能调用它编译时类型的方法,而不能调用它运行时类型的方法,如果需要让这个引用变量来调用它运行时类型的方法,则需将它强制类型转换为运行时类型,使用方法:(type)variable.
①基本类型之间的转换只能在数值类型之间进行,数值类型不能和布尔类型之间进行类型转换。
②引用类型之间的转换只能把一个父类变量转换为子类类型。
可以通过下面的方式来避免强制类型转换中出现的ClassCastException,如:
Objectobj = “hello”;
String str =(String )obj;
可以用以下的语句替代,以检测是否会出现ClassCastException
if(objinstanceof String)
{
String str = (String )obj;
}
☆instanceof运算符的前一个操作数通常是一个引用类型的变量,后一个操作数通常是一个类,用于判断前面的对象是否是后面的类或者其子类的实例。如果是则返回true,否则返回false。
4.6 继承和组合
4.6.1 继承
继承的缺陷:继承后的子类能访问父类的属性和方法,破坏了父类封装性,造成了子类和父类的严重耦合。因此父类的设计通常遵循以下原则:
①尽量隐藏父类的内部数据。尽量将父类所有的属性都设置成private访问类型。
②不让子类随意访问、修改父类的方法。
a)父类中仅为辅助其他的工具方法,应该使用private访问控制符进行修饰;
b)父类中需要被外部类调用的方法,以public修饰;
c)不希望子类重写,使用final修饰;
d)如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,则使用protected修饰。
e)尽量不要在父类构造器中调用将要被子类重写的方法。
代码分析:当系统创建Sub对象时,先会执行其父类的构造器,如果父类的构造器调用了被其子类重写的方法,则会调用被子类重写后的方法—调用②号方法,而此时Sub对象的name属性是null,因此将会引发空指针异常。
注意:上述这点与C++不一样,对于非虚函数,C++构造函数只能调用该类提供的方法;对于虚函数,子类若未重写该方法,则会调用父类提供的方法,否则调用子类中所重写的方法。
4.6.2 组合
如果需要复用一个类,除了把该类当成基类来继承之外,还可以将该类当成另外一个类的组合成分,从而允许新类直接复用该类的public方法。组合则是把旧类对象作为新类的属性嵌入以实现新类的功能。
总之,继承表达出来的是一种“是(is-a)”的关系,组合表达的是“有(has-a)”的关系。
4.7 初始化块
与java的构造器作用类似,也是对java对象进行初始化操作,初始化顺序严格按照程序书写的顺序进行。语法规则如下:
[修饰符] {
//初始化块的可执行性代码
…………………..
}
初始化块和构造器之间的区别:
初始化块总是在构造器前面执行。初始化块是一段固定执行的代码,不接受任何参数。
用static修饰的初始化块是静态初始化块,用于对整个类进行初始化,通常用于对类属性执行初始化处理。静态初始化块也被称为类初始化块,属于类的静态成员,同样要遵循非静态成员的规则:不能访问非静态成员,包括不能访问实例属性和实例方法。
普通初始化块的执行顺序:创建一个java对象时,先执行java.lang.Object类的初始化块、构造器,再执行父类的初初始化块、构造器…最后执行该类的初始化块和构造器。
静态初始化块的执行顺序:从java.lang.Object类到其父类的静态初始化块…最后才执行该类的静态初始化块。实例代码如下:
class Root
{
{
System.out.println("Root普通初始化块");
}
static
{
System.out.println("Root static初始化块");
}
Root()
{
System.out.println("Root 构造函数");
}
};
classMid extendsRoot
{
{
System.out.println("Mid普通初始化块");
}
static {
System.out.println("Mid static初始化块");
}
Mid()
{
System.out.println("Mid缺省构造函数");
}
Mid(String msg)
{
System.out.println("Mid带参构造函数,参数为:"+msg);
}
};
classLeaf extendsMid
{
{
System.out.println("Leaf普通初始化块");
}
static {
System.out.println("Leaf static初始化块");
}
Leaf()
{
super("Leaf调用父类构造函数");
System.out.println("Leaf缺省构造函数");
}
};
public class HelloWorld
{
public static void main(String[] args)
{
System.out.println("构造第一个Leaf*************");
new Leaf();
System.out.println("构造第二个Leaf*************");
new Leaf();
}
}
执行结果如下:
构造第一个Leaf*************
Root static初始化块
Mid static初始化块
Leaf static初始化块
Root普通初始化块
Root 构造函数
Mid普通初始化块
Mid带参构造函数,参数为:Leaf调用父类构造函数
Leaf普通初始化块
Leaf缺省构造函数
构造第二个Leaf*************
Root普通初始化块
Root 构造函数
Mid普通初始化块
Mid带参构造函数,参数为:Leaf调用父类构造函数
Leaf普通初始化块
Leaf缺省构造函数
执行结果分析:
①由于statci初始化块属于类,因此在生成新的对象时,首先执行的是static程序块,且只执行一次。
②同一个类中,先执行初始化块,再执行构造函数。
③构函数的重载。按照重载规则,调用对应的构造函数。