注:博主是看《Java疯狂讲义》进行复习和总结的,所以博客里面的代码和举例部分是来自原书。加一些自己的个人理解在里面。。因为自己的总结还不够成熟,里面有些说法可能是不对的,希望大家看到错误指出来,我一定会改正 (づ ̄ 3 ̄)づ
面向对象的三大特征:封装,继承,多态
一、 类
类的成员包括:构造器,成员、方法(初始化块)
static:可以修饰方法和成员变量(以及初始化块)。静态成员不能直接访问非静态成员。
Static的做用是区分构造器、成员、实例、初始化块这四种东西是属于类还是属于类的实例化。被静态关键字修饰的部分是属于类的构造器:
构造器的修饰词public private protect 三个选其中一个。也可以省略。
构造函数名称要求与类名相同,定义方法形参列表格式完全相同,不能定义返回值类型。
注意:不能定义返回值类型并不意味着构造函数是没有返回值的。因此不可以使用void去声明。(如果使用了void去声明,那么这个构造函数将不再拥有构造函数的功能,而是变成一个普通函数)
关于构造器的返回值:构造器的返回值是隐式的。使用new关键字的时候会调用构造函数,这时候构造函数的返回值是类的实例。其类型永远是当前类。无需定义
二、 对象(类的实例)
基本概念:对象可以调用方法,成员等。(Static修饰的方法成员可以使用类直接调用)
类不是一种具体的存在,实例才是。(就好比人类和具体的人,具体的人才是具体的存在)深入理解对象(从存储角度):
首先先提一下指针。关于指针,许多人在学习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只能放在方法的第一行,否则报错。