java基本特征(封装、继承、多态)

来源:互联网 发布:ipadhd软件 编辑:程序博客网 时间:2024/05/16 11:20

JavaSE知识总结

1.面向对象之封装、继承、多态

Java是一种面向对象设计的高级语言,支持继承、封装和多态三大基本特征,首先我们从面向对象两大概念:类和对象(也称为实例)谈起。来看看最基本的类定义语法:

/*命名规则:*类名(首字母大写,多个单词组合时每个单词首字母大写,单词之间不加任何连接符号)*字段名、方法名(首字母小写,多个单词组合时第一个单词首字母小写,之后每个单词首字母大写,单词之间不加任何连接符号)*/[public][final] 类名 {    [public|protected|private 类名() {}] //构造器    [public|protected|private][static|final] 类型 字段名 [=默认值];//字段列表    [public|protected|private][static|final|abstract] 返回值 方法名(参数) {} //方法列表}

在上面的基本语法中,我们发现以下几个知识点:

  1. 构造器名称为类名相同且没有任何返回值(甚至都不能返回void)
  2. 字段可以添加默认值
  3. 字段与方法都可以使用static进行修饰
  4. 类的修饰符要么为public,要么没有
  5. 方法的修饰符中final和abstract不能同时使用

此处,我将会着重介绍前面三条,后面两条我会在后面给大家总结出来。

首先是为什么不构造器没有任何返回值?

java中当使用new关键字来调用构造器时是有返回值的,总是返回当前的实例对象,因此无需定义返回值类型。 当我们利用构造器来实例化对象的时候,如果没有提供构造器,系统会隐式的为我们提供一个无参构造器,如果我们自己提供了一个构造器,那么系统将不会为我们提供构造器,所以当我们自己提供一个带参构造器的时候,最好手动写上无参构造器。

但是请注意了:虽然构造器负责创建对象,但是它不全是为了创建对象而存在的,实际上当调用构造器时,系统会为创建的对象分配内存空间并执行默认化操作,即在构造器执行之前,对象就已经产生,只是不能被访问,只能在构造器中用this来引用,当构造器执行完成之后,当前这个对象才能被外界访问。

不能理解我在说什么?好,我给大家举一个例子来加深大家对该知识的理解。

public class Encapsulation {    public static void main(String[] args) {        Person p=new Person();        System.out.println(p.getName());    }}/**Person类*/class  Person{    private String name="qinling";    private int age=23;    public String getName() {        return name;    }    public Person(){        System.out.println("构造器中的对象"+this);    }}

执行后,大家会发现在person的构造器中能够给输出当前对象,说明在构造器执行之前已经存在这个对象了,new对象的时候只是将这个对象返回给使用者。

下面给大家介绍一下类的存储关系,还是上面的例子,我们给出如下的关系图:

从上面可以看出,

对引用类型来说,堆内存仅仅存放的是引用类型的变量名,而堆内存中存放的是对象的实际数据,通过引用变量名来引用实际的对象,如果将该变量赋值给另一个引用变量,仅仅是将两个引用变量指向了同一个对象而已,而不会发生赋值对象数据

我们来谈论下一个知识点,java方法的参数传递问题,这个也是一个需要注意的地方。

public class TestPassPrimitiveArgs {    public static void swap(int a, int b) {        System.out.println("交换前,a = " + a + ", b = " + b);//a = 3,b = 5        int temp = a;        a = b;        b = temp;        System.out.println("交换后,a = " + a + ", b = " + b);//a = 5,b = 3    }    public static void main(String[] args) {        int a = 3;        int b = 5;            swap(a, b);        System.out.println("交换结束后,a = " + a + ", b = " + b);//a = 3,b = 5    }}

说明什么?

说明Java方法的参数传递方式只有一种:值传递,即将实参的副本传递给方法,参数本身不发生任何变化

从运行结果来看,在swap()中交换之前是3和5,交换后变成5和3,而实参在main()中始终变成不变,因此在main()传递给swap()的实参只是a和b的副本,而不是a和b本身。我们以内存分布来说明执行状况,当在main()中传参给swap()时,实际上就是在main()方法栈区向swap()方法栈区传递一份a和b的副本,如下图:

当执行swap()时,swap()方法栈区将a和b副本进行交换,交换完成后进入main()方法栈区,此时仅仅a和b的副本发生改变,其本身没有发生任何变化。接下来我们来看看引用类型的交换,前面我们说了只能通过值传递的方式来传参,可能对有些朋友来说稍显疑惑。

class DataSwap {    public int a;    public int b;}public class TestPassReferenceArgs {    public static void swap(DataSwap ds) {        System.out.println("交换前,ds.a = " + ds.a + ", ds.b = " + ds.b);//ds.a = 3, ds.b = 5        int temp = ds.a;        ds.a = ds.b;        ds.b = temp;        System.out.println("交换后,ds.a = " + ds.a + ", ds.b = " + ds.b);//ds.a = 5, ds.b = 3    }    public static void main(String[] args) {        DataSwap ds = new DataSwap();        ds.a = 3;        ds.b = 5;        swap(ds);        System.out.println("交换结束后,ds.a = " + ds.a + ", ds.b = " + ds.b);//ds.a = 5, ds.b = 3    }}

从运行结果来看,确实不仅在swap中交换成功,在main中仍然是交换之后的结果。让人一下觉得:从main中传递给swap似乎不是ds对象的副本了,而是ds本身,这与我们前面谈到的Java方法传参只能按值传递相违背了。

下面我详细说明一下,直接上图说明问题:

我们都知道,此时传递的是引用类型DataSwap,而引用类型的内存方式已经谈过了,在main()方法栈区中实际存放的是ds对象的地址,而实际的数据(a,b)是存放在堆内存中。现在将ds对象引用由main传递给swap,实际上是ds对象的地址复制一份到swap方法栈区中,此时main和swap中都已拥有ds对象的地址,且都指向在堆内存中实际存放的数据。也就是说引用类型参数数据传递方式是不折不扣的值传递方式,只不过传递的仅仅是引用变量,而不是引用变量所指向的引用类型数据。当然这里对main或swap中任何一个ds对象数据的更改都会影响到另一方,同时我们还可以验证main和swap中的ds是两个不同的引用变量,试着在swap种方法最后添加: ds=null.也就是切断swap中对ds的引用,查看一下main中ds对象的a和b是否受到影响(结果是不会)。

接下来,我们来谈一谈可变参数的实现方法(就是在类型后面添加三个点...),看实际的例子

public class TestVarityArgs {    public static void readBooks(String name, String... books) {        System.out.println(Arrays.toString(books));        if(books.length == 0) {            System.out.println(name + " has not a book to read");        } else {            String result = name + " is reading: ";            for(String book : books) {                result += book + " ";            }            System.out.println(result);        }    }    public static void main(String[] args) {        readBooks("qinling");        readBooks("qinling", "Java", "oracle", "J2EE");        readBooks("qinling", new String[] { "Java", "oracle", "J2EE" });    }}

谈到了可变参数,似乎跟重载函数非常相似,都是同一个方法有多种调用形式,但是它们有着显著的区别。重载函数必须满足"两同一不同":同一个的重载方法名的必须相同,但是形参列表不同(返回值、修饰符不能作为重载的标准)。请注意,尽量别对包含可变参数的方法进行重载,因为这样可能会引起歧义。

前面一直提到static这个概念,接下来我以例子来说明它的应用,可以看出static和非static字段和方法的区别所在。

public class TestCat {    public static void main(String[] args) {        Cat cat = new Cat("tomcat", 15);        cat.display();        //static方法可通过类调用,也可通过实例调用,调用效果一致,会对该类的所有实例产生影响        cat.AddCourse(1);//p.AddCourse(1);        cat.display();        //cat1.courses现在也变成3        Cat cat1 = new Cat("HelloKitty", 14);        cat1.display();    }}class Cat {    public String name;    public int age;    public static int courses = 2;    public Cat() {}    public Cat(String name, int age) {        this.name = name;        this.age = age;    }    public static void AddCourse(int count) {        //在static方法中只能访问static字段,不能访问实例字段        //System.out.println(name + " 's course: ");        courses += count;    }    public void display() {        //在实例方法中可以访问static字段        System.out.println("Name: " + name + ", Age: " + age + ", Course: " + courses);    }}

那在实际开发中,怎样例区分static和非static的引用呢?

简单的建议是:如果定义的变量是用来描述每个对象的固有信息(如每个人都有姓名、年龄),则应该使用实例变量,相反如果描述的类的固有信息(如只要是人就只能有两只眼睛),则应该使用类变量

下面开始讲解重点知识:

封装

封装就是将对象的属性等信息隐藏在类的内部,仅提供给外部一些满足预设条件的方法供调用。拿上面的例子来说明:每个人的年龄只能在0~150之间来进行浮动,现在的情况是我可以随意更改年龄(想多少岁就多少岁),那肯定就不对了。我们必须将这些不满足条件的操作及时的过滤掉,Java提供了访问权限控制: private->default->protected->public(权限依次扩大)来封装内部属性和提供外部接口(对字段采用private或protected等修饰符来限制,采用getter和setter来进行有效控制)

关于访问控制符,有以下建议:类的绝大部分字段(有些少数的static字段需要public修饰)和辅助方法都采用private来修饰,并提供getter和setter访问器来对其读取和修改;如果值希望同一个包中其他类访问,则不添加任何修饰符;如果只希望子类也能使用父类的成员而不被外界知晓,则采用protected来修饰;如果可以在任何地方都能访问到,则采用public来修饰

继承

继承就是在已有类的基础上扩展新的子类,而不改变原有父类的数据和行为,即我们通常所说的父类和子类(遵从"子类 is a 父类"原则),子类可继承父类的非私有成员(建议将成员修饰符改为protected),同时也可重写父类相同的成员

遵从"两同两小一大"原则:即方法名相同,方法形参相同;子类方法返回类型比父类更小或相等,子类方法抛出的异常比父类更小或相等;子类方法的访问权限比父类更大或相等,重写的方法要么都是类方法,要么都是实例方法,如父类有一个实例方法,子类添加一个同名的类方法则不算重写,而是子类的新方法

Father.java

public class Father {    private String name;    private int age;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }    public void display(){        System.out.println("father-->display");    }}

Child.java

public class Child extends Father {    @Override    public void display() {//重写了父类的方法        System.out.println("child-->display");    }    public static void main(String[] args) {        Child c=new Child();        c.setAge(12);        c.setName("child");        System.out.println(c.getAge());        System.out.println(c.getName());        c.display();    }}

如果在子类中先调用已重写的父类方法,该怎么办呢?Java提供了super引用,指向其直接父类的默认引用。当创建一个对象时,系统会隐式创建其父类的对象(Java所有类都继承自java.lang.Object),只要该类有子类存在,就一定会产生super引用,指向其对应的直接父类,当子类方法中使用某个成员变量时,首先会查找当前类中是否存在,如不存在则查找直接父类中是否存在,如不存在会依次追溯到java.lang.Object中是否存在,如仍然不存在将不能通过编译。

我们将上面的代码稍作调整Father c=new Child();

运行之后我们会发现,虽然是父类对象,但是还是调用了子类的方法。

则我们有如下的结论:

当引用变量的编译时类型和运行时类型不一致(父类 t = new 子类();)时,我们说表现出了对象的多态。而多态仅仅表现在调用重写方法时,将调用子类中的方法,调用非重写方法时,如果该方法在父类中将调用父类,如果在子类中将无法调用,而对象的字段不具多态性,即只能调用父类中对应的字段。

到现在为止,基本讲完了面向对象的三大特性,在结束本篇讲解之前,简单谈一下"初始化块"的应用。

所谓初始化块,就是在构造器执行之前,对整个类(所有对象)的字段进行初始化的过程,通常会将多个构造器中相同的部分放到初始化块中执行,可以把初始化块看成是没有形参的方法,只不过在构造器执行之前执行而已。与构造器执行顺序一致,初始化块也遵循从父类到子类依次执行的过程。与初始化块对应的还有静态初始化块,主要完成类属性的初始化,并且只在类加载时初始化一次。

Father.java

public class Father {    static {        a = 4;        System.out.println("Father的静态初始化块执行");    }    {        b = 4;        System.out.println("Father的初始化块执行");    }    public static int a = 2;    public int b = 3;    public Father() {        System.out.println("Father的构造器执行");    }}

Child.java

public class Child extends Father {    static {        // a = 10;        System.out.println("Child的静态初始化块执行");    }    {        b = 6;        System.out.println("Child的初始化块执行");    }    public Child() {        System.out.println("Child的构造器执行");    }    public static void main(String[] args) {        for(int i = 0; i < 2; i++) {            Child c = new Child();            System.out.println("a: " + Father.a + ", b: " + c.b);        }    }}

运行结果:

Father的静态初始化块执行Child的静态初始化块执行Father的初始化块执行Father的构造器执行Child的初始化块执行Child的构造器执行a: 2, b: 6Father的初始化块执行Father的构造器执行Child的初始化块执行Child的构造器执行a: 2, b: 6

从运行结果来看:静态初始化块和初始化块都先于构造器执行,并都遵从父类到子类的执行过程,但静态初始化块最先执行且仅执行一次,子类初始化块在父类的初始化块和构造器执行完毕之后,在子类构造器之前执行

OK,到此为止,面向对象的三大基本特征就总结完成。希望大家能自行仔细琢磨一下其中原理.

0 0
原创粉丝点击