3.1 类与对象
3.1.1 面向对象的三大特征
封装、继承、多态
封装:对象内部的操作对外部而言不可见
继承:利用现有类的功能,在这个类的基础上进行功能的扩展
多态:一个类实例的相同方法在不同情况下有不同的表现形式,可以使得不同的外部对象共享相同的外部接口。
3.1.2 类与对象的基本概念
类:事物拥有的共同特性和操作的行为的集合,由方法和属性组成。方法确定操作的行为,属性是一些变量,用于描述对象的具体特点。
对象:类抽象出来的具体的可以使用的事物
3.1.3 类的定义与实体化
一个类的完整定义如下:
class 类名称 { 属性1; 属性2; 属性n...;
方法1(){} 方法2(){} 方法n(){}... } |
这个时候,类内的方法不再由主类直接调用,而需要通过实例化的对象调用。
这里举一个实例:
class Person{ public String name; public int age; //属性 public Person(String name,int age){ this.name = name ; this.age = age ; } //方法1 public String getPersonInfo(){ return "姓名:"+this.name+",年龄:"+this.age; } //方法2 } |
创建类的定义之后,就可以创建对象了:
类名称 对象名称 = new 类名称(); |
例如上面的Person类,我们就可以实例化一个对象出来,同样也可以通过对象来调用实例变量和方法:
Person p1 = new Person(); Person p2 = new Person("Steven",25); |
Person p = new Person("Steven",25); System.out.println(p.name); System.out.println(p.getPersonInfo()); |
同时也可以构造匿名对象:
new Person("张三",20).getPersonInfo(); |
匿名对象使用起来和普通对象一致,但是只能使用一次
3.1.4 对象相关的内存分析
首先,类创建后不会占用内存,要通过实例化对象后才能分配内存。当创建一个对象后,例如:
Person per = new Person();
此时会先在栈上创建一个对象,同时在堆上开辟一块内存用于存放数据,并将栈上的对象指向内存中堆的数据。由于这个语句只是创建对象,并没有初始化,所以里面的内容都是默认值。需要注意的是:对象必须要在实例化之后使用,否则会产生NullPointerException,这个错误只会在运行中发生。
实例化之后我们就可以修改一些属性了,这个时候要注意要用对象名来引用属性值了:
per.name = “AX” ; per.age = 18 ; |
上面只是对一个对象进行的内存分析,那么当两个对象间出现了引用传递,又会发生什么?考虑下面的代码:
Person per1 = new Person(); Person per2 = new Person(); per2 = per1 ; |
首先,前两行代码都会创建一个对象,每个对象都会在栈内存和堆内存创建空间,并分别有对应的指向。
第三行代码发生了引用传递,类比于数组的引用传递,per2的栈内存将会指向per1所在的堆内存的区域,同时也就失去了对原堆空间的使用权,原per2的堆内存就成为了一个垃圾内存。这种情况应该避免,因为所有的垃圾内存会不定期被Java虚拟机收回,但是收回的过程会影响程序的性能。在开发中要尽可能少的出现垃圾内存。
而对于匿名对象,由于不会有任何的占空间指向它,所以使用一次后就成为垃圾空间。
3.1.5 封装和构造方法
为什么要存在封装?来看一个例子
class Person{ String name; int age; public void getPersonInfo(){ System.out.println("姓名:"+name+",年龄:"+age); } } public class Test{ public static void main(String[] args) { Person person = new Person(); person.name = "张三" ; person.age = -200 ; person.getPersonInfo(); } } |
在主程序中,首先先建立了一个对象,但是我们可以对这个类内的元素随意修改,这样实际上对于程序来说是不正常的,这个时候就需要一种保护措施,为了能够防止在类外访问这个类的情况(内部操作对外部不可见),这个时候就需要进行封装。
private 实现封装
class Person{ private String name; private int age; public void getPersonInfo(){ System.out.println("姓名:"+name+",年龄:"+age); } } |
这个时候我们可以对属性进行封装,private关键字可以使得这个属性为私有状态,但是这样也会使得我们访问这个元素变得困难。为了访问这个私有属性,按照Java的设计源自,我们需要提供两种方法来获得以及修改数据。
getter方法:用于属性内容的获取
setter方法:用于对属性内容的设置或者修改
class Person{ private String name; private int age; public void setName(String n){ name = n ; } public String getName(){ return name; } public void setAge(int i){ if (i>0&&i<=200) { age = i ; }else { age = 0 ; } } public int getAge(){ return age; } public void getPersonInfo(){ System.out.println("姓名:"+name+",年龄:"+age); } } |
有了上面的内容,可以总结类的设计原则:
1、编写类时,类中的所有的属性都必须使用private封装;
2、属性若要被外部访问,则必须要定义setter和getter方法。
访问控制权限
变量或者方法的访问控制权限一共有四种,他们之间的访问范围大小关系有:private<protected<default<public。各自的访问范围如下表所示。
关键字\范围 | 当前类 | 同一包内 | 子类/子类的子类 | 其他包 |
private | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
public | √ | × | × | × |
3.1.6 构造方法
构造方法是使用关键字new实例化新对象时来进行调用的操作方法。对于构造方法的定义,也需要遵循以下原则:
1.方法名称必须与类名称相同
2.构造方法没有返回值类型声明
3.每一个类中一定至少存在一个构造方法(没有明确定义,则系统自动生成一个无参构造)
这里需要强调的是,编译器是根据程序结构来区分普通方法与构造方法的,所以即便构造方法无返回值,但是也不能有void的声明。当类中定义了构造方法,则默认的午餐构造方法将不再生成。
构造方法一般用于给类中的属性进行初始化的操作。
class Person{ private String name; private int age;
public Person(String n,int i){ name = n ; setAge(i); }
public void setName(String n){ name = n ; } public String getName(){ return name; } public void setAge(int i){ if (i>0&&i<=200) { age = i ; }else { age = 0 ; } } public int getAge(){ return age; }
public void getPersonInfo(){ System.out.println("姓名:"+name+",年龄:"+age); } } public class Test{ public static void main(String[] args) { Person person = new Person("张三",-200); person.getPersonInfo(); } } |
构造方法的重载
构造方法也是可以重载的,可以实现不同情况下的构造方法的实现。
public Person(){ System.out.println("===无参构造==="); } public Person(String n){ name = n ; System.out.println("===有参构造==="); } |
3.1.7 this关键字
this关键字可以用在以下三种情况:
1、this调用本类属性
class Person{ private String name; private int age;
public Person(String name,int age){ name = name ; age = age ; } public String getPersonInfo(){ return "姓名:" + name + ",年龄:"+age; } } public class Test{ public static void main(String[] args) { Person per = new Person("张三",20); System.out.println(per.getPersonInfo()); } } |
注意构造方法这里,当参数与类中的属性重名的时候,我们就无法对类中属性进行赋值,此时我们要加上this关键字表明即可。
this.name = name ; this.age = age ; |
2、this调用本类方法
this调用的本类方法既可以调用普通方法,也可以调用构造方法,但两者格式有区别。
this调用普通方法:this.方法名(参数) | this调用构造方法:this(参数) |
class Person{ private String name; private int age;
public Person(String name,int age){ this.name = name ; this.age = age ; this.print();//调用普通方法 } public String getPersonInfo(){ return "姓名:" + name + ",年龄:"+age; } public void print(){ System.out.println("**********"); } } public class Test{ public static void main(String[] args) { Person per = new Person("张三",20); System.out.println(per.getPersonInfo()); } } | public Person(){ System.out.println("***产生一个新的Person对象***"); }
public Person(String name){ this();//调用本类无参构造 this.name = name ; }
public Person(String name,int age){ this(name);//调用本类有参构造 this.age = age ; } |
虽然调用本类的普通方法是可以不加上this关键字的,但是仍然需要添加用于表明方法定义的来源是那个类,在类的继承中有作用。
当this调用构造方法时需要注意:this调用构造方法的语句必须在构造方法首行,使用this调用构造方法时,请留有出口。
3、this表示当前对象
只要对象调用了本类中的方法,这个this就表示当前执行的对象。
3.1.8 static关键字
static关键字可以修饰类内的属性或者方法。
static类属性
类中如果有属性被static修饰,那么这个属性就会放在内存中的全局数据区作为共享属性,所有对象都可以进行该数据区的访问。一旦要修改static类属性的值,所有对象都会同步此属性值。
static类方法
使用static定义的方法,直接通过类名称来访问。某些方法不希望收到对象的控制,既可以在没有实例化对象的时候执行,这种作坊尝尝存在于各种工具类中。
所有的static方法不允许调用非static定义的属性或方法,所有的非static方法允许访问static方法或属性。
为AB类的一个无形式参数无返回值的方法method书写方法头,可以用AB.method()方式调用,该方法头的形式为( )。
|
正确答案: A 你的答案: B (错误) static void method( ) public void method( ) final void method( ) abstract void method( ) |
3.1.9 代码块
用{}括起来的定义的一段代码,根据代码块的位置和关键字的不同,可以分为以下四种:
普通代码块、构造块、静态块、同步代码块
普通代码块:定义在方法中的代码块,一般如果方法中代码过长,为了避免变量重名会使用普通代码块,实际应用中使用较少。
构造块:定义在类中,且不加修饰符的代码块。
class Person{ { //定义在类中,不加任何修饰符,构造块 System.out.println("1.Person类的构造块"); } public Person(){ System.out.println("2.Person类的构造方法"); } }
public class Test{ public static void main(String[] args) { new Person(); new Person(); } } |
对于构造块要注意以下几点:
1、构造块优先于构造方法执行;
2、每产生一个新的对象就调用一次构造块;
3、构造块可以进行简单的逻辑操作。
静态代码块:使用static定义的代码块,但是静态代码块的位置(主类中/非主类中)会导致不同的结果。一般情况下静态块都是在静态变量之后定义,因为有可能静态变量的初始化工作需要在静态块中进行。
非主类中的静态块 | 主类中的静态块 |
class Person{ { //定义在类中,不加任何修饰符,构造块 System.out.println("1.Person类的构造块"); } public Person(){ System.out.println("2.Person类的构造方法"); } static { //定义在非主类中的静态块 System.out.println("3.Person类的静态块"); } }
public class Test{ public static void main(String[] args) { System.out.println("--start--"); new Person(); new Person(); System.out.println("--end--"); } } | public class Test{ { System.out.println("1.Test的构造块"); } public Test(){ System.out.println("2.Test的构造方法"); } static{ System.out.println("3.Test的静态块"); }
public static void main(String[] args) { System.out.println("--start--"); new Test(); new Test(); System.out.println("--end--"); } } |
对于非主类中的静态代码块,静态块优先于构造块执行。此外无论产生给多少实例化对象,静态块都只执行一次。
对于主类中的静态块,优先于主方法(main)执行。
总的来说,各类代码块的执行顺序是:
静态库(包含需要初始化的静态变量、静态块)->构造块->构造方法,静态库内部的顺序按照代码书写的顺序决定。
主类静态块->主方法
下列代码的执行结果是 public class B { public static B t1 = new B();//静态变量 public static B t2 = new B(); { System.out.println("构造块");//构造块 } static { System.out.println("静态块");//主类静态块 } public static void main(String[] args) { B t = new B();//main方法 } } |
A.静态块 构造块 构造块 构造块 B.构造块 静态块 构造块 构造块 C.构造块 构造块 静态块 构造块 D.构造块 构造块 构造块 静态块 |
What will happen when you attempt to compile and run the following code? public class Test{ static{ int x=5; } static int x,y; public static void main(String args[]){ x--; myMethod( ); System.out.println(x+y+ ++x); } public static void myMethod( ){ y=x++ + ++x; } }
|
正确答案: D compiletime error prints:1 prints:2 prints:3 prints:7 prints:8 |
3.1.10 内部类
内部类指的是再一个类的内部进行其他类结构的嵌套的操作,内部类会破坏程序的结构,但是可以方便的操作外部类的私有访问。
Outter.Inner in = new Outter().new Inner();
class Outer{ private String msg = "Hello World" ; class Inner{ //定义一个内部类 public void print(){ //定义一个普通方法 System.out.println(msg); //调用msg属性 } } // ******************************** //在外部类中定义一个方法,该方法负责产生内部类对象并且调用print()方法 public void fun(){ Inner in = new Inner(); //内部类对象 in.print(); // 内部类提供的print()方法 } }
public class Test{ public static void main(String[] args) { Outer out = new Outer(); //外部类对象 out.fun(); //外部类方法 } } |
内部类存在的原因:
1、内部类方法可以访问该类定义所在作用域中的数据,包括被 private 修饰的私有数据
2、内部类可以对同一包中的其他类隐藏起来
3、内部类可以实现 java 单继承的缺陷
4、当我们想要定义一个回调函数却不想写大量代码的时候我们可以选择使用匿名内部类来实现
class Outer{ private String msg = "Hello World" ; public String getMsg(){ //通过此方法才能取得msg属性 return this.msg ; } public void fun(){ //3.现在由out对象调用了fun()方法 Inner in = new Inner(this); //4.this表示当前对象 in.print(); //7.调用方法 } }
class Inner{ private Outer out; public Inner(Outer out){ //5.Inner.out = mian.out this.out = out ; //6.引用传递 } public void print(){ //8.执行此方法 System.out.println(out.getMsg()); } }
public class Test{ public static void main(String[] args) { Outer out = new Outer(); //1. 实例化Outter类对象 out.fun(); //2.调用Outter类方法 } } |
内部类与外部类的关系:
1、对于非静态内部类,内部类的创建依赖外部类的实例对象,在没有外部类实例之前是无法创建内部类的
2、内部类是一个相对独立的实体,与外部类不是is-a关系
3、内部类可以直接访问外部类的元素(包含私有域),但是外部类不可以直接访问内部类的元素
4、外部类可以通过内部类引用间接访问内部类元素
class Outter { private String outName; private int outAge;
class Inner { private int InnerAge; public Inner() { Outter.this.outName = "I am Outter class"; Outter.this.outAge = 20; } public void display() { System.out.println(outName); System.out.println(outAge); } } } public class Test { public static void main(String[] args) { Outter.Inner inner = new Outter().new Inner(); inner.display(); } } |
内部类的分类:
在Java中,内部类分为成员内部类、静态内部类、方法内部类、匿名内部类
成员内部类
成员内部类依附于外围类,要先创建外围类才能创建内部类,此外成员内部类中不能存在任何的static的方法和变量。
静态内部类
内部类用static关键字修饰成为静态内部类,静态内部类没有保存指向创建它的外围类的引用,所以静态内部类的创建不需要依赖外围类。而静态内部类不可以使用任何外围的非static成员变量和方法。
class Outer{ private static String msg = "Hello World" ; // ******************************** static class Inner{ //定义一个内部类 public void print(){ //此时只能使用外部类中的static System.out.println(msg); //调用msg属性 } } // ******************************** //在外部类中定义一个方法,该方法负责产生内部类对象并且调用print()方法 public void fun(){ Inner in = new Inner(); //内部类对象 in.print(); // 内部类提供的print()方法 } }
public class Test{ public static void main(String[] args) { Outer.Inner in = new Outer.Inner(); in.print(); } } |
方法内部类
方法内部类只能在这个方法中被使用,离开该方法就会失效,这种只是在一个局部区域来解决问题,但不希望这个类是公共可用的类称为局部内部类。
局部内部类对外部完全隐藏,且不允许使用任何访问权限修饰符(private/protected/public)。如果想使用方法形参,则这个形参必须要用final声明。
class Outter { private int num; public void display(int test) { class Inner { private void fun() { num++; System.out.println(num); System.out.println(test); } } new Inner().fun(); } }
public class Test { public static void main(String[] args) { Outter out = new Outter(); out.display(20); } } |
匿名内部类
没有名字的方法内部类,除了要满足方法内部类的各项特征外,还要注意:
1、匿名内部类是没有访问修饰符的;
2、匿名内部类必须继承一个抽象类或者实现一个接口;
3、匿名内部类中不能存在任何静态成员或方法;
4、匿名内部类是没有构造方法的,因为它没有类名;
5、与局部内部相同匿名内部类也可以引用方法形参。此形参也必须声明为 final。
关于匿名内部类叙述正确的是? ( ) |
正确答案: B 匿名内部类可以继承一个基类,不可以实现一个接口 匿名内部类不可以定义构造器 匿名内部类不能用于形参 以上说法都不正确 |
3.1.11 继承
继承的主要作用:在已有基础上继续进行功能的扩充。
继承的实现: class 子类 extends 父类
class Person{ 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; } } class Student extends Person{ //定义了一个子类 private String school; //扩充的新属性
public String getSchool() { return school; }
public void setSchool(String school) { this.school = school; } }
public class Test { public static void main(String[] args) { Student student = new Student(); student.setName("Steven"); student.setAge(18); student.setSchool("高新一中"); System.out.println("姓名:"+student.getName()+",年龄:"+student.getAge()+",学 校:"+student.getSchool()); } } |
继承关系一旦确定,子类可以直接继承父类的所有结构(私有属性、构造方法、普通方法),可以实现代码的复用。所有的非私有操作属于显示继承(可以直接调用),所有的私有操作属于隐式继承(通过其他形式调用,例如setter或getter)。除此之外,子类中还可以进行功能的扩充,扩充属性和方法。
class Person{ private String name;
public String getName() { return name; } public void setName(String name) { this.name = name; } } class Student extends Person{ public void fun(){ System.out.println(getName()); } }
public class Test { public static void main(String[] args) { Student student = new Student(); student.setName("Steven"); System.out.println(student.getName()); student.fun(); } } |
此时父类中的属性的确被子类所继承了,但是发现子类能够使用的是所有非private操作,而所有的private操作无法被直接使用,所以称为隐式继承。
继承使用的注意事项
1、子列对象实例化之前一定会先实例化一个父类对象,默认调用父类的构造方法之后才会调用子类的构造方法进行子类对象的初始化。
2、如果想在子类中调用父类的属性或方法,则需要用到关键字super.方法() 或 super.属性名,如果调用的是构造方法,则直接使用super() 或者super(参数) 调用无参或者有参的构造方法。这种操作常出现于方法的覆写中。
3、Java只支持单继承,一个子类只能继承一个父类,但是可以通过多层继承的方式实现多继承。
class A{} class B extends A{} class C extends B{} |
子类A继承父类B, A a = new A(); 则父类B构造函数、父类B静态代码块、父类B非静态代码块、子类A构造函数、子类A静态代码块、子类A非静态代码块 执行的先后顺序是? |
正确答案: C 你的答案: B (错误) 父类B静态代码块->父类B构造函数->子类A静态代码块->父类B非静态代码块->子类A构造函数->子类A非静态代码块 父类B静态代码块->父类B构造函数->父类B非静态代码块->子类A静态代码块->子类A构造函数->子类A非静态代码块 父类B静态代码块->子类A静态代码块->父类B非静态代码块->父类B构造函数->子类A非静态代码块->子类A构造函数 父类B构造函数->父类B静态代码块->父类B非静态代码块->子类A静态代码块->子类A构造函数->子类A非静态代码块 |
3.1.12 覆写
如果子类定义了与父类相同的方法或者属性的时候,这样的操作成为覆写。属性覆写使用很少,不在这里讨论。
对于方法的覆写
子类定义了与父类方法名称、参数类型及个数完全相同的方法。但是被覆写的方法不能够拥有比父类方法更为严格的访问控制权限。
class Person{ public void print(){ System.out.println("1.[Person]类的print方法"); } }
class Student extends Person{ public void print(){ System.out.println("2.[Student]类的print方法"); } } public class Test{ public static void main(String[] args) { new Student().print(); } } |
重载与覆写的区别
重载 | 覆写 |
方法名相同,参数的类型和个数不同 | 方法名、参数列表、返回值类型完全一致 |
类内重载 | 子类覆写父类的方法 |
无权限要求 | 被覆写的方法不能够拥有比父类方法更为严格的访问控制权限。 |
this 与 super的区别
this关键字 | super关键字 |
访问本类中的属性和方法 | 由子类访问父类中的属性或者方法 |
先找本类,若没有则调用父类 | 直接查找父类 |
this用于表示当前对象 |
|
3.1.13 final关键字
final称为终结器,可以用来修饰类、方法、属性。
若final修饰一个类,这个类不能被继承,该类的所有方法默认都会加上final修饰。
若final修饰一个方法,这个方法不能被子类所覆写。
若final修饰一个变量,则这个变量就变成了常量,常量必须在声明时初始化,并且不能被修改。如果带有final修饰的变量参加数据类型转换,则转换不会生效,也就是说带final修饰的变量不能参与涉及到需要类型转换的运算中。
请选出下列程序出错的语句
byte b1=1,b2=2,b3,b6,b8; final byte b4=4,b5=6,b7=9; public void test() { b3=(b1+b2); /*语句1*/ b6=b4+b5; /*语句2*/ b8=(b1+b4); /*语句3*/ b7=(b2+b5); /*语句4*/ System.out.println(b3+b6); } |
3.1.14 与类相关的类型转换
转型分为两类:向上转型/向下转型。无论是哪种转型,一定发生在存在继承关系的父子类之间否则会产生ClassCastException。
向上转型指的是子类转型为父类
class Person{ public void print(){ System.out.println("1.我是爸爸!"); } }
class Student extends Person{ public void print(){ System.out.println("2.我是儿子!"); } } public class Test{ public static void main(String[] args) { Person per = new Student(); //向上转型 per.print(); } } |
不管是否发生了向上转型,核心本质还是在于:你使用的是哪一个子类(new在哪里),而且调用的方法是否被子类所覆写了。
向下转型指的是父类向子类转型,一般情况下,当需要子类扩充操作的时候就需要采用向下转型。
class Person{ public void print(){ System.out.println("1.我是爸爸!"); } }
class Student extends Person{ public void print(){ System.out.println("2.我是儿子!"); } public void fun(){ System.out.println("只有儿子有!"); } }public class Test{ public static void main(String[] args) { Person per = new Student(); per.print(); //这个时候父类能够调用的方法只能是本类定义好的方法 //所以并没有Student类中的fun()方法,那么只能够进行向下转型处理 Student stu = (Student) per; stu.fun(); } } |
但是向下转型存在一定的安全隐患,避免隐患的做法是先做判断,满足转型条件后再转型,可以以来关键字instanceof实现:
子类对象instanceof 类 ,返回值为boolean类型
Person per = new Student(); System.out.println(per instanceof Person); System.out.println(per instanceof Student); if (per instanceof Student) { //避免ClassCastException Student stu = (Student) per ; stu.fun(); } |