J
A
V
A
编
程
孙冬
类和对象的概念
类:表示一个共性的产物
对象:对象就是一个个性的体现
理解:
1.没有类则肯定没有对象的产生
2.没有对象的产生类不能使用(访问静态变量和方法除外)
3.对象的所有活动范围在类中已经完整的定义出来了
类实际上就是相当于是一个对象的操作图纸,按此图纸生产出来的对象,才可以被用户所使用。
新建一个Person类:
public class Person
{
String name ;
int age ;
public void tell(){
System.out.println("我的姓名:"+name+" "+"年龄:"+age);
}
}
内存空间对应如下:
在这个类中,我们定义了类的属性和方法,但是这个类还不能运行,因为:一、这个类没有主函数,程序无法运行;二、类中没有产生对象,所以不能访问类中的变量和方法。
下面介绍两种产生对象的方法,从而来使程序输出。
对象的产生分2个步骤:
声明对象:类名称对象名称= null ;
实例化对象:对象名称= new 类名称();
如果对象在使用中只是声明的话,则无法使用,必须实例化之后才可以正确使用。
1.在这个类的内部增加主函数和创建对象。
public class Person
{
String name ;
int age ;
public void tell(){
System.out.println("我的姓名:"+name+" "+"年龄:"+age);
}
public static void main(String args[]){
Person per = new Person();//创建一个Person对象
https://www.doczj.com/doc/316414356.html, = "张三";
per.age = 30;
per.tell();
}
}
提示:把https://www.doczj.com/doc/316414356.html, = "张三";per.age = 20;代码注释,看看程序输出结果。
2.再建一个ShangDi类,通过这个类来创建Person对象,
class Person
{
String name ;
int age ;
public void tell(){
System.out.println("我的姓名:"+name+" "+"年龄:"+age);
}
}
public class ShangDi
{
public static void main(String args[]){
Person per = new Person();
https://www.doczj.com/doc/316414356.html, = "张三";
per.age = 30;
per.tell();
}
}
注意:这个java文件包含了2个类,其中ShangDi是个公共类,所以这个java源文件的名字为ShangDi.java。
对象的内存空间图如下:
问题:已经知道类中的属性全部保存在堆内存中,那么全部的方法保存在什么地方?
全部的方法保存在全局代码区之中。
既然可以产生一个对象,那么就可以产生多个对象,多个对象之间会相互影响吗?
举例:
class Person
{
String name ;
int age ;
public void tell(){
System.out.println("我的姓名:"+name+" "+"年龄:"+age);
}
}
public class ShangDi
{
public static void main(String args[]){
Person per1 = new Person();
Person per2 = new Person();
https://www.doczj.com/doc/316414356.html, = "张三";
https://www.doczj.com/doc/316414356.html, = "李四";
per1.age = 30;
per2.age = 33;
per1.tell();
per2.tell();
}
}
运行结果可以看出没有影响,因为per1和per2是2个不同的对象,对它们是分别进行实例化的,内存图如下:
在对象的操作中实际上是存在引用传递的,所谓引用传递,就是将一个堆内存空间的使用权交给其他的对象,就相当于为一个堆内存空间起了一个别名。
举例:在这个例子中,我们将per1的引用赋值给了per2
class Person
{
String name ;
int age ;
public void tell(){
System.out.println("我的姓名:"+name+" "+"年龄:"+age);
}
}
public class ShangDi
{
public static void main(String args[]){
Person per1 = new Person();
Person per2 = null;
per2 = per1;
https://www.doczj.com/doc/316414356.html, = "张三";
per1.age = 30;
per2.age = 33;
per1.tell();
per2.tell();
}
}
从结果可以看出,per1和per2指向的是同一个对象,彼此之间产生影响。
内存图如下:
Java继承:
Java继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
JA V A不支持多重继承,单继承使JA V A的继承关系很简单,一个类只能有一个父类. 继承的格式:
class 子类extends 父类{}
举例:
class Person
{
String name="张三";
int age;
public void tell(){
System.out.println("Person的tell方法");
}
Person(){
this("李四");
System.out.println("Person的构造函数");
}
Person(String ss){
System.out.println(ss);
}
}
class Student extends Person
{
String name;
String school;
Student(){
//super();
System.out.println("Student的构造函数");
}
public void tell(){
System.out.println("Student的tell方法");
}
}
public class JiChengDeamo
{
public static void main(String args[]){
Student stu = new Student();
stu.tell();
System.out.println(https://www.doczj.com/doc/316414356.html,);
}
}
构造器(构造函数)
?当一个类实例化时,调用的第一个方法就是构建器
?构建器是方法的一种,但它和一般方法有着不同的使命。
?构建器(constructor)是提供对象初始化的专用方法。它和类的名字相同,但没有任何
返回类型,甚至不能为void类型。
?构建器在对象创建时被自动调用,它不能被显式地调用。
?Java 中的每个类都有构建器,用来初始化该类的对象。
?可以在构建器中利用this关键字调用类中的其他构建器。
?一般方法可以被子类继承,而构建器却不可被继承。
?构建器只能由new运算符调用。
?如果一个类中没有明确声明一个构造方法,则会自动生成一个无参什么也不做的隐
藏构造方法
举例:
public class Person
{
String name ;
int age ;
public Person(){
System.out.println("*******");
}
public void tell(){
System.out.println("我的姓名:"+name+" "+"年龄:"+age);
}
public static void main(String args[]){
Person per = new Person(); //创建一个Person对象
https://www.doczj.com/doc/316414356.html, = "张三";
per.age = 30;
per.tell();
}
}
结论:即使没有调用person()函数,但函数仍然执行,而且还是先执行。Person()函数的执行是在程序执行了new Person()后,自动调用的。
构造器的重载
必须要明白重载的概念!!!
举例:
public class Person
{
String name ;
int age ;
public Person(){
System.out.println("*******");
}
public Person(String ss){
name = ss;
System.out.println(name);
}
public Person(String ss,int aa){
name = ss;
age = aa;
System.out.println(name+" "+age);
}
public void tell(){
System.out.println("我的姓名:"+name+" "+"年龄:"+age);
}
public static void main(String args[]){
Person per1 = new Person(); //创建一个Person对象
Person per2 = new Person("张三"); //创建一个Person对象
Person per3 = new Person("李四",22); //创建一个Person对象
}
}
输出结果为:
---------- JA V A ----------
*******
张三
李四22
输出完成(耗时0 秒) - 正常终止
想想为什么???
构造函数重载的原则和普通方法一致
如果一个类中已经明确的声明了一个构造函数,则不会再重新生成无参的什么都不做的构造方法
变量初始化的3种方法:
在构造器中设置值
在声明中设置值
初始化块
在一个类的声明中可以包含多个代码块,只要构造类的对象,这些块就会被执行。这种机制是不常见的。
举例:
class Person
{
String name;
static int age = 1;
{
age = age+1;
}
}
public class InitDemo1
{
public static void main(String args[]){
Person per1 = new Person();
Person per2 = new Person();
System.out.println(Person.age);
}
}
因为age是静态的,所有变量共享age,而每创建一个对象都会执行一次代码块,当per1被创建时,age值为2,当per2被创建时,age值为3.
举例:
class Person
{
String name;
static int age = 1;
public Person(){
System.out.println("****");
this.age = 2;
}
{
System.out.println("####");
age = age+1;
}
}
public class InitDemo1
{
public static void main(String args[]){
Person per1 = new Person();
Person per2 = new Person();
System.out.println(per2.age);
}
}
从结果可以看出,在初始化的时候,执行顺序为:先在声明中初始化,在执行初始化块,最后再执行构造函数!
静态代码块:代码块前面加stati c关键字,每当加载一个类的时候,需要执行静态代码
块,以后创建对象的时候,不需要再执行静态代码块。
举例:
class Person
{
String name;
static int age = 1;
public Person(){
System.out.println("****");
this.age = 2;
}
{
System.out.println("####");
age = age+1;
}
static{
System.out.println("@@@@");
}
}
public class InitDemo1
{
public static void main(String args[]){
Person per1 = new Person();
Person per2 = new Person();
System.out.println(per2.age);
}
}
从结果可以已看出:
1.虽然创建2个对象,但静态代码块只被执行一次,而且是在创建对象之前执行
2.不管是代码块还是静态代码块,可以写在程序的任意位置,程序执行时,根据书写
顺序执行,即如果有2个代码块,先执行写在前面的代码块,再执行写在后面的代码块(代码块和静态代码块要分开,静态代码块是在加载类的时候执行,只执行一次,代码块是在创建对象的时候执行,每创建一个执行一次)
Java类初始化深入分析初始化(initialization)其实包含两部分:
1.类的初始化(initialization class & interface);
2.对象的创建(creation of new class instances)。
类的初始化其实是类加载(loading of classes),它是"对象的创建"的第一步。
顺序:类的加载肯定是第一步的,所以类的初始化在前,然后是对象的初始化。大体的初始化顺序是:类初始化->对象的初始化
举例:
class Bowl {
Bowl(int marker) {
System.out.println("Bowl(" + marker + ")");
}
void f1() {
System.out.println("Ok");
}
}
class Cupboard {
Bowl b1 = new Bowl(1);
static Bowl b2 = new Bowl(2);
{
System.out.println("Cupboard()");
}
static Bowl b3 = new Bowl(3);
}
class Table {
Table() {
System.out.println("Table()");
}
Table(String a,int i){
this();
System.out.println("Ok");
}
}
public class Order {
static Cupboard t1 = new Cupboard();
static Table t2;
Bowl t3 = new Bowl(10);
public static void main(String[] args) {
System.out.println("Creating new Cupboard() in main");
new Cupboard();
System.out.println("Creating new Cupboard() in main");
Order od = new Order();
od.t3.f1();
Table t4 = new Table("aa",1);
}
}
运行结果:
Bowl(2)
Bowl(3)
Bowl(1)//变量初始化完毕之后调用构造方法
Cupboard()
Creating new Cupboard() in main
Bowl(1)//第二次初始化非静态变量,静态变量初始化一次Cupboard()
Creating new Cupboard() in main
Bowl(10)
Ok
Table()
Ok
分析:首先程序从public类开始,装载Order,class,运行static Cupboard t1 = new Cupboard(); 由于t1是一个对象引用变量,首先得到初始化,然后转到Cupboard类的实例中,这时候程序开始初始化该实例中的变量,按照先static后非static变量初始化的顺序原则开始初始化各个变量,此时,
static Bowl b2 = new Bowl(2);
static Bowl b3 = new Bowl(3);
这两块先得到初始化然后Bowl b1 = new Bowl(1);得到初始化(注意,这是非stataic变量)然后转到Cupboard() 构造函数。此时Cupboard类已经初始化完毕,程序返回到public 类中,又发现有static Table t2; 开始初始化,由于t2只是个声明的变量,并没有创建一个Table对象让它指向,因此不要管这个t2变量。在public类中,由于此时再没有static变量可以初始化,那么程序转到static方法main中进行,(注意:Bowl t3 = new Bowl(10); 这一句为什么得不到初始化,是因为此时没有创建类Order的实例,所以程序目前还不会初始化该代码,至于以后会不会初始化,还要看在main方法中是否会创建Order类的实例,如创建了,则一定初始化,否则不会。)
在main方法中,
System.out.println("Creating new Cupboard() in main");//首先打印该行要输出的语句new Cupboard(); //创建了Cupboard类的实例,程序转到Cupboard中进行变量初始化,此时要注意:static变量的初始化,它只能初始化一次,也就是说,如果前面已经初始化过了,那么此时就不必要再初始化了,这一点要牢记!!!System.out.println("Creating new Order() in main");//程序转入此行打印要输出的语句Order od = new Order();//创建Order对象实例,程序转入实例od中,此时Bowl t3 = new Bowl(10); 这一句的t3会得到初始化,程序转到Bowl 类中进行有关的操作,操作完后返回到main方法中od.t3.f1(); //调用实例t3中的方法f() Table t4 = new Table("aa",1); //创建Table类的实例,让t4指向这个实例,t4得到初始化,调用Table含有参数的构造方法,程序进行下去。
最后再总结一下:
初始化顺序是:
(1)先是父类的static变量和static初始化块
(2)然后是子类的static变量和static初始化块
(3)父类的实例变量、初始化快
(4)父类的构造方法
(5)子类的实例变量、初始化快
(6)子类构造方法
封装的意义:
封装是指隐藏对象的属性和实现细节,仅仅对外公开接口。封装能为软件系统带来以下优点:一、封装
封装就是将属性私有化,提供公有的方法访问私有的属性。
*实现封装的步骤:
(1)修改属性的可见性来限制对属性的访问。
(2)为每个属性创建一对赋值方法和取值方法,用于对这些属性的访问。
(3)在赋值和取值方法中,加入对属性的存取限制。
**为了实现良好的封装性,我们通常将类的成员变量声明为private,再通过
public的方法来对这个变量进行访问。对一个变量的操作,一般都有读取和
赋值操作,我们分别定义两个方法来实现这两种操作,一个是getXxx()(Xxx
表示要访问的成员变量的名字),用来读取这个成员变量操作,另外一个是setXxx()用来对这个成员变量赋值。
**如果外面的程序可以随意修改一个类的成员变量,会造成不可预料的程序错误,就象一个人的身高,不能被外部随意修改,只能通过各种摄取营养的方法去修
改这个属性。
class Person
{
private String name ;
private int age ;
public void tell(){
System.out.println(this.getName()+" "+this.age);
}
public void setName(String name){
if (name.length()<2||name.length()>4)
{
System.out.println("名字不合要求");
}else{
https://www.doczj.com/doc/316414356.html, = name;
}
}
public String getName(){
if (name==null)
{
return "请重新输入姓名";
}else{
return https://www.doczj.com/doc/316414356.html,;
}
}
public void setAge(int age){
if (age<0||age>120)
{
System.out.println("年龄不合理!");
}else{
this.age = age;
}
}
public int getAge(){
return this.age;
}
}
public class Demo1
{
public static void main(String args[]){
Person per = new Person();
per.setName("张三");
per.setAge(-50);
per.tell();
}
}
****封装的优点:
(1)隐藏类的实现细节;
(2)让使用者只能通过事先定制好的方法来访问数据,可以方便地加入控制逻辑,限制对属性的不合理操作;
(3)便于修改,增强代码的可维护性;
考虑一种关系(重点)
现在要求类表示如下的关系:
一个人有一本书,一本书属于一个人
按照类的划分,可以划分出两个操作类:Person、Book
class Person{
private String name ;
private int age;
private Book book ;// 一个人有一本书
public Person(String n,int a){
this.setName(n) ;
this.setAge(a) ;
}
public void setBook(Book b){
book = b ;
}
public void setName(String n){
name = n ;
}
public void setAge(int a){
age = a ;
}
public Book getBook(){
return book ;
}
public String getName(){
return name ;
}
public int getAge(){
return age ;
}
};
class Book{
private String title ;
private float price ;
private Person person ;
public Book(String t,float p){
this.setTitle(t) ;
this.setPrice(p) ;
}
public void setPerson(Person p){
person = p ;
}
public void setTitle(String t){
title = t ;
}
public void setPrice(float p){
price = p ;
}
public Person getPerson(){
return person ;
}
public String getTitle(){
return title ;
}
public float getPrice(){
return price ;
}
};
public class Demo04{
public static void main(String args[]){
Person per = new Person("张三",30) ;
Book bk = new Book("Java基础",89.0f) ;
per.setBook(bk) ; // 一个人有一本书
bk.setPerson(per) ; // 一本书属于一个人
System.out.println(per.getBook().getTitle()) ; // 由人找到其所拥有书的名字
System.out.println(bk.getPerson().getName()) ; // 由书找到人的名字}
}
进一步延伸:
一个人不光有一本书,还可能有一个孩子,那么此种关系又该如何表示呢?class Person{
private String name ;
private int age;
private Book book ;// 一个人有一本书
private Person child ;
public Person(String n,int a){
this.setName(n) ;
this.setAge(a) ;
}
public void setChild(Person c){
child = c ;
}
public Person getChild(){
return child ;
}
public void setBook(Book b){
book = b ;
}
public void setName(String n){
name = n ;
}
public void setAge(int a){
age = a ;
}
public Book getBook(){
return book ;
}
public String getName(){
return name ;
}
public int getAge(){
return age ;
}
};
class Book{
private String title ;
private float price ;
private Person person ;
public Book(String t,float p){
this.setTitle(t) ;
this.setPrice(p) ;
}
public void setPerson(Person p){
person = p ;
}
public void setTitle(String t){
title = t ;
}
public void setPrice(float p){
price = p ;
}
public Person getPerson(){
return person ;
}
public String getTitle(){
return title ;
}
public float getPrice(){
return price ;
}
};
public class RefDemo05{
public static void main(String args[]){
Person per = new Person("张三",30) ;
Person chd = new Person("张四",10) ;
Book bk = new Book("Java基础",89.0f) ;
Book bkc = new Book("童话故事",89.0f) ;
per.setChild(chd) ;
chd.setBook(bkc) ;
bkc.setPerson(chd) ;
per.setBook(bk) ; // 一个人有一本书
bk.setPerson(per) ; // 一本书属于一个人
System.out.println(per.getBook().getTitle()) ; // 由人找到其所拥有书的名字
System.out.println(bk.getPerson().getName()) ; // 由书找到人的名字}
};
运行时多态性是面向对象程序设计代码重用的一个最强大机制,多态性的概念也可以被说成“一个接口,多个方法”。Java实现运行时多态性的基础是动态方法调度,它是一种在运行时而不是在编译期调用重载方法的机制,下面就继承和接口实现两方面谈谈java运行时多态性的实现。
一、通过继承中超类对象引用变量引用子类对象来实现
举例说明:
//定义父类SuperA
class SuperA
{
int i = 100;
void fun(){
System.out.println("This is SuperA");
}
}
//定义SuperA的子类SubB
class SubB extends SuperA
{
int m = 1;
void fun(){
System.out.println("This is SubB");
}
}
//定义superA的子类subC
class SubC extends SuperA
{
int n = 1;
void fun(){
System.out.println("This is SubC");
}
}
class Demo2
{
public static void main(String[] args){
SuperA a;
SubB b = new SubB();
SubC c = new SubC();
a=b;
a.fun(); //(1)
a=c;
a.fun(); //(2)
}
}运行结果为:
This is SubB
This is SubC
上述代码中SubB和SubC是超类SuperA的子类,我们在类Test中声明了3个引用变量a, b, c,通过将子类对象引用赋值给超类对象引用变量来实现动态方法调用。也许有人会问:“为什么(1)和(2)不输出:This is S uperA”。java 的这种机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
所以,不要被上例中(1)和(2)所迷惑,虽然写成a.fun(),但是由于(1)中的a被b赋值,指向了子类SubB的一个实例,因而(1)所调用的fun()实际上是子类SubB的成员方法fun(),它覆盖了超类SuperA的成员方法fun();同样(2)调用的是子类SubC的成员方法fun()。
另外,如果子类继承的超类是一个抽象类,虽然抽象类不能通过new操作符实例化,但是可以创建抽象类的对象引用指向子类对象,以实现运行时多态性。具体的实现方法同上例。
不过,抽象类的子类必须覆盖实现超类中的所有的抽象方法,否则子类必须被abstract 修饰符修饰,当然也就不能被实例化了。
二、通过接口类型变量引用实现接口的类的对象来实现
接口的灵活性就在于“规定一个类必须做什么,而不管你如何做”。我们可以定义一个接口类型的引用变量来引用实现接口的类的实例,当这个引用调用方法时,它会根据实际引用的类的实例来判断具体调用哪个方法,这和上述的超类对象引用访问子类对象的机制相似。
举例说明:
//定义接口InterA
interface InterA
{
void fun();
}
//实现接口InterA的类B
class B implements InterA{
public void fun(){
System.out.println("This is B");
}
}
//实现接口InterA的类C
class C implements InterA{
public void fun(){
System.out.println("This is C");
}
}
class Demo3
{
public static void main(String[] args)
{
InterA a;
a= new B();
a.fun();
a = new C();
a.fun();
}
}
输出结果为:
This is B
This is C
上例中类B和类C是实现接口InterA的两个类,分别实现了接口的方法fun(),通过将类B和类C的实例赋给接口引用a而实现了方法在运行时的动态绑定,充分利用了“一个接口,多个方法”展示了Java的动态多态性。
需要注意的一点是:Java在利用接口变量调用其实现类的对象的方法时,该方法必须已经在接口中被声明,而且在接口的实现类中该实现方法的类型和参数必须与接口中所定义的精确匹配。
结束语:以上就是java运行时多态性的实现方法,大家在编程过程中可以灵活运用,但是在性能要求较高的代码中不提倡运用运行时多态,毕竟Java的运行时动态方法调用较之普通的方法调用的系统开销是比较大的。
子类角度看
当你要用的功能子类没有而父类有时,你就有需要向上转型
父类角度
当你要用的功能父类没有而子类有时,你就有需要向下转型
我们在Java编程中经常碰到类型转换,对象类型转换主要包括向上转型和向下转型。
向上转型
我们在现实中常常这样说:这个人会唱歌。在这里,我们并不关心这个人是黑人还是白人,是成人还是小孩,也就是说我们更倾向于使用抽象概念“人”。再例如,麻雀是鸟类的一种(鸟类的子类),而鸟类则是动物中的一种(动物的子类)。我们现实中也经常这样说:麻雀是鸟。这两种说法实际上就是所谓的向上转型,通俗地说就是子类转型成父类。这也符合Java提倡的面向抽象编程思想。来看下面的代码:
package a.b;
public class A {
public void a1() {
System.out.println("Superclass");
}
}
A的子类B:
package a.b;
public class B extends A {
public void a1() {
System.out.println("Childrenclass"); //覆盖父类方法
}
public void b1(){} //B类定义了自己的新方法
}
C类:
package a.b;
public class C {
public static void main(String[] args) {
A a = new B(); //向上转型
a.a1();
}
}
如果运行C,输出的是Superclass 还是Childrenclass?不是你原来预期的Superclass,而是Childrenclass。这是因为a实际上指向的是一个子类对象。当然,你不用担心,Java虚拟机会自动准确地识别出究竟该调用哪个具体的方法。不过,由于向上转型,a对象会遗失和父类不同的方法,例如b1()。有人可能会提出疑问:这不是多此一举吗?我们完全可以这样写:B a = new B();
a.a1();
确实如此!但这样就丧失了面向抽象的编程特色,降低了可扩展性。其实,不仅仅如此,向上转型还可以减轻编程工作量。来看下面的显示器类Monitor:
package a.b;
public class Monitor{
public void displayText() {}
public void displayGraphics() {}
}
液晶显示器类LCDMonitor是Monitor的子类:
package a.b;
public class LCDMonitor extends Monitor {