面向对象(上)

注:博主是看《Java疯狂讲义》进行复习和总结的,所以博客里面的代码和举例部分是来自原书。加一些自己的个人理解在里面。。因为自己的总结还不够成熟,里面有些说法可能是不对的,希望大家看到错误指出来,我一定会改正 (づ ̄ 3 ̄)づ

面向对象的三大特征:封装,继承,多态

一、 类

  1. 类的成员包括:构造器,成员、方法(初始化块)

  2. static:可以修饰方法和成员变量(以及初始化块)。静态成员不能直接访问非静态成员。
    Static的做用是区分构造器、成员、实例、初始化块这四种东西是属于类还是属于类的实例化。被静态关键字修饰的部分是属于类的

  3. 构造器:
    构造器的修饰词public private protect 三个选其中一个。也可以省略。
    构造函数名称要求与类名相同,定义方法形参列表格式完全相同,不能定义返回值类型。
    注意:不能定义返回值类型并不意味着构造函数是没有返回值的。因此不可以使用void去声明。(如果使用了void去声明,那么这个构造函数将不再拥有构造函数的功能,而是变成一个普通函数)
    关于构造器的返回值:构造器的返回值是隐式的。使用new关键字的时候会调用构造函数,这时候构造函数的返回值是类的实例。其类型永远是当前类。无需定义

二、 对象(类的实例)

  1. 基本概念:对象可以调用方法,成员等。(Static修饰的方法成员可以使用类直接调用)
    类不是一种具体的存在,实例才是。(就好比人类和具体的人,具体的人才是具体的存在)

  2. 深入理解对象(从存储角度):
    首先先提一下指针。关于指针,许多人在学习Java的时候总会听到一些人说,Java中是没有指针的。我觉的这种说法不是很正确。如果不把指针当成具体的工具来看,其实指针的用法存在于大部分语言。Java将其封装,使操作者不能直接使用指针访问内存。并不能说Java里就是没有指针的。

class Person{
    String name;
    int age;
    Person(){
        ……;
    }
}

Person p = new Person();
在这行代码里,左边是定义了一个Person类型的p变量,并对Person进行实例化赋值给p变量。
从内存的角度来看,p变量是存储在栈中的,而 new Person()产生的Person类的实例是存放在堆之中的。Java程序是不允许直接访问堆内存中的数据。因此这是赋值号“=”可以看做是将Person类的实例的地址赋给了p变量,栈内存中的p变量指向堆内存中的数据。
(从这个角度来看java是存在指针的。。)
对内存中的对象可以有多个引用,比如:Person p1 = p;
这样的操作并不会使堆内存中在产生新的数据,而是在栈内存中新创建一个p1变量,这个p1变量也指向堆内存中Person的实例。

3.this关键字:指向调用该方法的对象。
作用:使类中的一个方法访问该类里的另一个方法或者实例变量。
this关键字可以省略。
static与this关键字不可以同时使用(静态成员不可以访问非静态成员):this可以代表任何对象。当this出现在某个方法体中时,所代表的对象是 不确定的,但他的类型是确定的,它所代表的的对象类型只能是当前类,只有当这个方法被调用的时候,this所代表的对象才能确定下来。谁调用了这个方法,this就代表谁。从这个角度也可以解释为什么this和static不能同时使用。static修饰的方法和成员是属于类的,this修饰的是属于类的实例的。

三、方法
方法不可以独立存在,必须被定义在类里。逻辑上可以分为属于类的方法,还是属于对象的方法。使用static修饰的方法是属于类的。

从内存角度来看,Java方法中的参数传递机制:
1.参数传递机制:值传递
值传递:将实际参数值的副本传入方法中,而参数本身不会受到影响。(意思是把这个值复制了一部分,并不是把这个值自己传进去进行操作。引用类型传递的时候,传进去的是对象的引用的副本,对象本身还在堆内存。)
引用类型的参数传递机制也是属于值传递。
举例:

Class Swap{
    public static void swap(int a,int b){
        int t = 0;
        t = a;
        a = b;
        b = t;
        System.out.println("在swap方法中a和b的值分别为:"+a"  "+b);
    }
    public static void main(){
        int a = 1,b = 2;
        System.out.println("在main函数中调用swap方法之前a和b的值分别为:"+a"  "+b);
        Swap s = new Swap();
        s.swap(a,b);
        System.out.println("在main函数中调用swap方法之后a和b的值分别为:"+a"  "+b);

    }
}

从以上程序中的结果可以得到,Java的参数传递类型是值传递。在main函数中的输出结果可以得到,a和b的值并没有互换。
这是因为
在main的方法栈区中,a的值为1,b的值为2。当调用swap方法的时候,会在内存中开辟一片swap的方法栈区,将a的值和b的值传入swap方法,而不是将a和b两个变量传递到swap方法中。因此在swap方法中a和b交换之后,swap方法中a的值变成2,b的值变成1。但是main方法中的a和b变量不会受到影响。因为传递进去的只是a和b的值而已,和这两个变量本身没有什么关系。(这就是形参实参的区别。。可以用这个解释吧)

再来看容易被人误解的引用类型的参数传递机制
代码举例:

class DataWrap{
    int a;
    int b;
}
public class ReferenceTransferTest{
    public static void wsap(DataWrap dw){
        int t = dw.a;
        dw.a = dw.b;
        dw.b = t;

    }
    public static void main(){
        DataWarp dw = new DataWarp();
        de.a = 6;
        dw.b = 9;
        swap(dw);
        System.out.println("交换结束后,a成员变量的值是"+dw.a+";b成员变量的值是"+dw.b);
    }

}
//运行结果:
//swap方法里,a成员变量的值是9,b成员变量的值是6
//main方法里,交换结束后,a成员变量的值是9,b成员变量的值是6

按照前一个基本类型参数传递方法来看,第二个例子里面不应该是main方法里互换之后a和b的值并没有发生变化吗?那是不是引用类型参数传递的时候就不是值传递,是把原来的对象传进去了呢?
答案是否定的。
其实引用类型传递进去的,并不是dw对象自身,是它的副本。那么为什么会结果显示交换成功呢?

程序从main方法开始执行,main方法开始创建了一个DataWarp对象,并另一了一个dw引用变量来指向DataWarp对象。这是一个与基本类型不同的地方。创建一个对象时,系统内存中有两个东西:一个是堆内存中保存了对象的本身,另一个是占内存中保存了引用该对象的引用变量。
接着程序通过引用来操作DataWarp对象,把该对象的a、b两个成员分别赋值6,9 。
接下来main方法中调用swap方法,main方法此时还没有结束,系统会为mian方法、swap方法开辟分别的栈区,用于存放main。swap的局部变量。
调用swap方法的时候,dw变量作为实参传入swap()方法,同样是采用值传递方式:把main方法中的dw变量的值赋给swap方法的形参,从而完成swap方法中形参的初始化。main方法中的dw是一个引用(指针)。它保存的是DataWarp对象的地址值,当把dw的值赋给swap方法的dw形参之后,其实就是让swap中的dw形参也保存了这个地址值,这时swap中的dw对象也引用了堆内存中的DataWarp对象。(一个对象可以有多个引用)
因此这种引用类型的参数传递方式也是值传递,在传递的过程中,并不是把堆内存中的对象传递到形参里面,而是传递的对象的地址值(引用),也就是说是传进去了一个副本。(参数传递哪一部分,就好像是 赋值语句:形参 = 实参)。系统复制的是引用变量,而非赋值了对象。
在进行交换操作的时候,无论是形参还是实参,他们指向的都是对内存中同一个对象,因此使用形参对内存中的交换,实参指向的还是那个内存,因此main方法中的交换结果是9 , 6 。
如果要证明调用swap方法时传递进去的是引用的副本,那么在swap方法中最后一行,将dw值赋值成null,再进行main方法的输出,会发现并不影响输出结果。从这一角度来看是两个引用,销毁了其中一个,另外一个并不受到影响。

2.形参个数可变的使用方法
修饰符 方法名(类型 形参1,类型 形参2,类型… 形参3)
在形参列表中最后一种定义方式就是形参个数可变,在传入值的时候根据传值的个数来定。
例如(int a,String… books)和(int a,String[] books)的效果是一样的。
使用上的限制:形参个数可变的方法必须放在形参列表的最后申明。

3.递归方法
递归如果用一句话来解释就是:这个方法调用了它本身
不过需要注意的是递归是有出口的,就是在调用的时候要接近所要求得的结果,不然就会变成死循环。
(不过说起来简单做起来还是有些难啊,有许多算法题是用递归解决就很简单,但是对我来说还是不太好思考~以后加油)

4.方法重载
方法同名,参数列表中形参个数、种类不同就可以发生方法重载。需要注意的是,返回值的类型不可以作为重载的标准。

四、成员变量和局部变量

Java允许局部变量和成员变量同名。方法里面的局部变量会覆盖成员变量的值,如果需要调用成员变量,在方法里面使用this:
this.变量名 这时候代表的就是成员变量。·

1.基本定义
成员变量指的是在类里定义的变量。局部变量指的是在方法里面定义的变量。
成员变量被分为类变量和实例变量两种,定义成员变量时没有被static修饰的就是实例变量,反之就是类变量。类变量的生存周期和类相同,实例变量的生存周期和实例的生存范围相同。
局部变量分为三种,形参。方法局部变量。代码块局部变量。

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

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

举例:

class Person{
    public String name;
    public static int eyeNum;
}

…………
//创建第一个Person对象。
Person p1 = new Person();
//创建第二个Person对象
Person p2 = new Person();
//分别为两个Person对象的name实例变量赋值
p1.name = "张三";
p2.name = "李四";
//分别为两个Person的类变量赋值
p1.eyeNum = 2;
p1.eyeNum = 3;

分析这个创建的过程中内训的情况:
当程序第一次执行Person p1 = new Person();的时候,如果是代码第一次使用Person类,则系统通常会在第一次使用Person类的时候加载并初始化这个类。在类的准备阶段,系统将会为该类的类变量分配内存空间,并制定默认初始值。当Person完成初始化后,堆内存中有Person对象以及类成员变量eyeNum。系统会给类成员变量赋值为默认值。
当创建p1的时候,栈内存中开辟出p1的空间,堆内存中产生实例变量name,然后p1引用实例变量。这个时候堆内存中实例变量的值是null(引用类型变量系统赋值的初始值是null),当使用p1.实例变量进行赋值的时候,堆内存中的实例变量才有值。
再次创建p2的时候,在占内存中开辟一块区域存放p2,p2指向对内存中的Person对象。但是注意实例变量会分别是两个实例的,类变量是一个类专属的,因此堆内存中的情况是里面放置了一个类成员变量,两个实例变量。

3.局部变量的初始化和内存机制

局部变量定以后,必须经过显式初始化才能使用。系统不会为局部变量进行初始化。
这就意味着定义局部变量之后系统不会立即为局部变量分配空间,而是等其初始化的时候才会为它分配内存,并将初始值保存在这块内存中。
与成员变量不同,局部变量不属于任何类或者实例 因此它总是保存在它所在的方法栈中。如果局部变量是基本类型,那么这个变量里存的是相对应的值。如果是引用类型,那么变量里存的是地址,通过该地址引用到该变量实际引用的对象或者数组。
栈内存中的变量无须系统回收,往往随方法结束或者代码块结束而结束。因此局部变量的作用域是从初始化变量开始,知道该方法或该代码块运行完成而结束。因为局部变量只保存基本类型的值或对象的引用,因此局部变量所占用的内存通常比较小。
能使用代码块局部变量的地方,坚决不要使用方法局部变量。

五、访问控制

访问控制符有三种,public private protect 。还有不写出来默认的default
这个好像没什么说的。。什么情况下该用什么,多敲敲代码就知道怎么用了。(敲代码这个我练的还少=。=以后会努力的)

六、封装,继承,多态

1.封装
。。好像没啥可说的。。

2.继承
Java中只有单继承,没有多继承。一个类可以有间接的多个父类。
继承时,子类可以重写父类的方法,这时父类里面的同名方法将会被覆盖。如果需要使用到父类的同名方法,可以使用super来调用。
和this一样,super也是不能和static同时存在的。
super与this也是不可以同时存在的。
类里的方法和变量被覆盖之后就相当于隐藏了,但是依旧会分配内存空间的。

3.多态
多态分为两种,运行时多态和编译时多态。
运行时多态:方法的重载就是一种
编译时多态:由于编译时的类型和运行时的类型不一致导致的。
比如:父类 对象1 = new 子类();

代码举例:

class Father {
    int length = 185;
    void fufunc() {
        System.out.println("父亲个子很高");
    }
    void fu() {
        System.out.println("父亲在上班");

    }

}

class Son extends Father{
    int length = 175;
    void fufunc() {
        System.out.println("儿子的个子也很高");
    }
    void zi() {
        System.out.println("儿子在上学");
    }

}

public class Demo1 {

    public static void main(String[] args) {
        Father f = new Father();
        Son s = new Son();

        System.out.println("父亲和儿子的身高分别是:"+f.length+"  "+s.length);
        //调用父类的方法
        f.fufunc();
        //调用子类中重写父类的方法
        s.fufunc();
        //调用父类没有被自己重写的方法
        f.fu();
        //调用子类中独有的方法
        s.zi();
        //编译时类型为父类,运行时类型为子类,这时发生向上转型,无须说明,系统自动转换
        /*从这个角度也可以说明子类是特殊的父类。
         * */

        Father f1 = new Son();
        //调用实例变量的时候还是调用的父类的实例变量
        System.out.println("f1对象此时的身高:"+f1.length);
        //调用被子类重写过的方法时,就会调用子类的方法
        /* 这说明实例变量是没有多态的,只要方法才会发生多态
         * 
         * */
        f1.fufunc();
        //如果这种多态发生时,f1对象去调用子类拥有而父类没有的方法,会报错
        //f1.zi();

        //发生向下转型的时候,需要强制转换,否则会报错
        //很明显上下关系是父类是下,子类是上
        Son s1 = (Son) new Father();
    }

}

4.引用变量类型的强制转换
基本类型的、
引用类型的强制转换的前提是他们之间有继承关系

5.instanceof运算符
作用:instanceof运算符的前一个操作数通常是一个引用类型变量,后一个通常是一个类(也可以是接口,把接口理解成一种特殊的类)用于判断前面的对象是否是后面的类,或者子类,实现类的实例。如果是返回true,否返回false
在强制类型转换前可以使用instanceof来判断是否能够发生转换。否则会发生ClassCastException异常。使用这个关键字可以增强代码健壮性。

6.组合与继承

继承虽然很大程度上提高了代码复用性,但是在一定程度上也破坏了封装。而且有的时候代码的复用是不需要依靠继承来完成的,这时候就可以使用组合来完成复用。
继承,两个类的关系是is-a
复用,两个类的关系是has-a
(这个以后代码多了怎么取舍也就自然明白了。。就不用书上的代码再举例子了)

六、初始化块

1.基本概念
Java构造器可以对单个对象进行初始化操作,使构造器先完成整个Java对象的状态初始化,然后它将Java对象返回给程序,从而让该Java对象的信息更加完整。和构造器作用类似的是初始化块
格式如下:

[修饰符]{
    //初始化块的可执行代码
}

初始化块的修饰符只能是static
被static修饰的初始化块成为静态初始化块。初始化块里的代码可以包含任何可执行语句,包括定义局部变量、调用其他对象那个的方法,以及使用分支循环语句等。
初始化块虽然也是Java类的一种成员,但它没有名字,不能通过类或者类的实例来初始化。初始化块只能在创建Java对象时隐式执行。而且在构造器之前执行。

2.静态初始化块(类加载时各部分初始化的顺序)

他们执行的原理用代码来解释:


class Root {
    static{
        System.out.println("Root的静态初始化块");      
    }
    {
        System.out.println("Root的普通初始化块");
    }
    public Root(){
        System.out.println("Root的无参构造函数");
    }
}
class Mid extends Root{
    static{
        System.out.println("Mid的静态初始化块");       
    }
    {
        System.out.println("Mid的普通初始化块");
    }
    public Mid(){
        System.out.println("Mid的无参构造函数");
    }
    public Mid(String msg){
        //通过this来调用同意类中重载的构造器
        this();
        System.out.println("Mid的带参构造函数,他的参数值:"+msg);
    }   

}

class Leaf extends Mid{
    static{
        System.out.println("Leaf的静态初始化块");      
    }
    {
        System.out.println("Leaf的普通初始化块");
    }
    public Leaf(){
        //通过super来调用父类的带参数构造函数
        super("疯狂Java讲义");
        System.out.println("执行Leaf的构造器2");
    }


}

public class Demo2 {

    public static void main(String[] args) {
        new Leaf();
        new Leaf();

    }

}
/*
执行结果:
第一次new Leaf()的执行结果:
Root的静态初始化块
Mid的静态初始化块
Leaf的静态初始化块
Root的普通初始化块
Root的无参构造函数
Mid的普通初始化块
Mid的无参构造函数
Mid的带参构造函数,他的参数值:疯狂Java讲义
Leaf的普通初始化块
执行Leaf的构造器

第二次执行new Leaf() 的结果:
Root的普通初始化块
Root的无参构造函数
Mid的普通初始化块
Mid的无参构造函数
Mid的带参构造函数,他的参数值:疯狂Java讲义
Leaf的普通初始化块
执行Leaf的构造器
*/

由上面的代码和结果来看,静态初始化块在类第一次被实例化的时候首先初始化,仅初始化这一次。普通初始化块则类实例化多少次,它就实例化多少次。和构造函数类似,但是在构造函数前面进行初始化。
一个子类有多个间接父类的时候,子类实例化则会先一层一层去寻找父类,先实例化最顶端的父类。
super只能放在方法的第一行,否则报错。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值