Java面向对象
一、类和对象
对象:对象是用计算机语言对问题域中事物的描述,对象通过属性和方法来分别对应事物具有的静态属性和动态属性。
类:类是用于描述同一类型的对象的一个抽象的概念,类中定义了这一类对象所因具有的静态和动态属性。
类可以看成一类对象的模版,对象可以看成该类的一个具体实例。
如学生是类,学生中的小明即是对象。小明有什么属性呢,如:姓名,年龄。小明有什么动态属性呢?如:显示姓名,显示年龄。
类与类之间的关系:
1、关联关系:一个类中的属性,是另一个类的对象。
2、继承关系:XX是一种XX。
3、聚合关系:聚集(队员和队长聚集成球队)和组合(几者密不可分,如头、手等组合成身体)
4、实现关系:跟接口有关。如果父类中的某个方法,每个子类都有不同的实现方式,那么把这个方法写在某个接口中,让子类去实现这个接口,重写接口中的方法。
Java类的定义:使用class定义类,并且定义成员变量与方法。
成员变量:成员变量可以是Java中任何一种数据类型,在定义成员变量时,可以对其初始化,也可以不对其初始化,如果不初始化,那么Java会使用默认的值对其初始化。这一点不同于方法中定义的局部变量,局部变量必须遵循先定义,再初始化,再使用的原则,如果不初始化直接使用,编译会报错。成员变量的作用范围为整个类。
Java中的引用:Java中除了基础类型之外的变量类型都称为引用类型。Java中的对象是通过引用对其操作的。
那么引用类型的变量,在内存中是如何分配的呢?
引用类型的变量,在内存中占两块内存。比如说下面这段代码
//声明了一个String类型的引用变量,但并没有使它指向一个对象。
//s是成员变量,成员变量位于栈内存
String s;
//使用new语句创建了一个String类型的对象并用s指向它,
//以后可以通过s完成对其的炒作
s = new String("Hello World");
可以这样解释:
image.png
这就是Java创建对象时,系统是如何为其分配内存的。
为什么对象放在堆内存中呢?这是因为堆内存是动态分配的,而对象也是在程序运行期间才会创建的。
如何在内存中区分类和对象呢?
类是静态的概念,位于代码区。而对象是new出来的,位于堆内存,类的每个成员变量在不同的对象中有不同的值(除了静态变量),而方法只有一份,执行的时候才占内存。关于这部分的内容,后面会做详细的解释。
对象的创建和使用:
1.必须使用new关键字创建对象。
2.使用对象.成员变量来引用对象的成员变量。
3.使用对象.方法(参数列表)来调用对象的方法。
4.同一个类的不同对象有不同的成员变量存储空间
5.同一个类的不同对象共享该类的方法。
使用代码和图解解释类和对象在内存中是如何分配的:
class C{
int I ;
int j ;
public static void main(String[] args){
C c1 = new C();
C c2 = new C();
}
}
类和对象在内存中的详细解释
如上图所示:类是静态的代码,存在于代码区。main方法中的c1 c2是局部变量,位于栈内存中,当new出对象时,c1,c2中的值是指向堆内存中对应对象的地址。而i 和 j是成员变量,位于堆内存中分配给对象的那块内存中。
构造方法 : 使用new + 构造方法创建一个新的对象。构造方法必须与类同名,并且没有返回值。是用来初始化对象的函数。对构造函数的具体解释如下:
class Person{
int id ;
int age;
public Person(int _id,int _age){
this.id = _id;
this.age = _age;
}
public static void main(String[] args){
Person tom = new Person(1,25);
}
}
构造方法创建对象时,内存变化
任何一个局部变量,都被分配在栈内存中,方法一旦执行完,局部变量被释放。
当没有为类编写构造函数时,编译器自动为其添加无参的构造函数。
实例: 对一小段代码进行内存分析:
class BirthData{
private int day;
private int month;
private int year;
public BirthData(int d,int m,int y){
day = d;
month = m;
year = y;
}
public void setDay(int d){
day = d;
}
public int getDay(){
return day;
}
public void setMonth(int m){
month = m;
}
public int getMonth(){
return month;
}
public void setYear(int y){
year = y;
}
public int getYear(){
return year;
}
public void display(){
System.out.println(day +" - " + month + " - "+ year);
}
}
public class Test{
public static void main(String[] args){
Test t = new Test();
int date = 9;
BirthData b1 = new BirthData(7,7,1970);
BirthData b2 = new BirthData(1,1,2000);
t.change1(date);
t.change2(d1);
t.change3(d2);
System.out.println("date = " + date);
d1.display();
d2.display();
}
public void change1(int i){
i = 1234;
}
public void change2(BirthData b){
b = new BirthData(22,2,2004);
}
public void change3(BirthData b){
b.setDay(22);
}
}
运行该程序,结果如下:
运行结果
诶?怎么跟我们想象中的不一样呢?data的值为什么没变还是9呢?b1怎么还是指向之前的对象呢?别急,一步步分析,分析完一切都明了了。
分析:1.前4句代码
前四句代码分析
当前四句代码执行完毕,栈内存中为构造方法创建的三个局部变量消失。
内存中的情况变为这样,如下图所示。
图片.png
-
test.change1(data);这句代码非常有迷惑性,一开始我也认为运行完这句代码,data的值为变为1234,其实不是,分析完就懂了,为什么经过这句代码,data的值并没有改变。
图片.png
当这句代码执行完成之后,内存中是这样的,如下图所示
图片.png
3.test.change2(b1); 调用change2,将b1传递给b,因此b一开始指向的是b1指向的对象,当走到b=new BirthData(22,2,2004);这句代码时,b指向了内存中的22,2,2004对象。
图片.png
4.test.change3(b2);
图片.png
这句代码执行完成内存中的情况如下:
图片.png
现在很清楚,打印结果应该是: 图片.png 没毛病~
方法重载(OverLoad)
一个类中可以定义有相同的名字,但参数不同的多个方法,调用时,会根据不同的参数列表选择不同的方法。
返回值不同但参数相同,这种不叫做方法重载,这种叫做重名,编译是无法通过的,其实只要记住一句话就不会出错:只要程序在编译时能分清调用的是哪个方法,这几个方法就构成重载。
this关键字
this是一个引用,指向自身对象的引用。
1.在类的方法定义中使用的this关键字代表使用该方法的对象的引用。
2.当必须指出当前使用方法的对象是谁时要使用this。
3.有时使用this可以处理方法中成员变量和参数重名的情况。
4.this可以看作是一个变量,它的值是当前对象的引用。
static关键字
1.在类中,用static声明的成员变量为静态成员变量,它是该类的公用变量,在第一次使用时被初始化,对于该类的所有对象来说,static成员变量只有一份。(对比:非静态的成员变量,每new出一个就有一份。)
- 用static声明的方法为静态方法,在调用该方法时,不会将对象的引用传递给它,所以在static方法中不可访问非static的成员。(静态方法不再是针对于某个对象调用,所以不能访问非静态成员)(对比:非静态方法,针对于某个对象调用。)
3.可以通过对象引用或类名(不需要实例化)访问静态成员。
看一段小程序:
public class Cat{
private static int sid = 0;
private String name;
int id;
Cat(String name){
this.name = name;
id = sid++;
}
public void info(){
System.out.println("My Name is "+ name + "No."+ id);
}
public static void main(String[] args){
Cat.sid = 100;
Cat mimi = new Cat("mimi");
Cat pipi = new Cat("pipi");
mimi.info();
pipi.info();
}
}
静态的成员变量放在数据区(data seg),当有一个Cat被new出来,数据区中多了一个静态的成员变量sid。静态变量只有一份,存在于数据区,id和name是非静态变量,每个cat都有一份。静态变量是属于某个类的,它不属于单独的某个对象。如何访问静态的对象呢?任何一个该类的对象都可以访问,访问的是同一块内存,没有对象同样可以访问,类名.静态变量即可访问。
创建Cat对象时的图示如下: image.png这里要注意的是:字符串常量位于数据区。
继承
Java中使用extends关键字来实现类的继承机制,通过基础,子类拥有了父类中所有成员变量和方法。Java只支持单继承,不支持多继承(一个子类只能有一个父类,一个父类可以派生出多个子类)。
从内存分析继承:看一个小程序
class Person{
private String name;
private int age;
public void setName(String name){this.name = name;}
public String getName(){return name;}
public void setAge(String age){this.age = age;}
public int getAge(){return age;}
}
class Student extends Person{
private String school;
public String getSchool(){return school;}
public void setSchool(String school){this.school = school;}
}
public class TestPerson{
public static void main(String[] args){
Student student = new Student();
student.setName("John");
student.setAge(18);
student.setSchool("SCh");
System.out.println(student.getName());
System.out.println(student.getAge());
System.out.println(student.getSchool());
}
}
图解继承
访问控制
Java的权限修饰符位于类的成员定义前,用来限定其他对象对该类成员对象的访问权限。 image.png注意:1.对于class的权限修饰只可以用public和default。
2.public类可以在任意地方被访问。
3.default类只可以被同一个包内部的类访问。
重写:
1.在子类中可以根据需要对从基类中继承的方法进行重写。
2.重写方法必须和被重写方法具有相同的方法名称,参数列表和返回类型。
3.重写方法不能使用比被重写方法更严格的访问权限。
super关键字
在Java中使用super来引用基类的成分。
看一段小程序:
class FatherClass{
public int value;
public void f() {
value = 100;
System.out.println("FatherClass Value : "+ value);
}
}
class ChildClass extends FatherClass{
public int value;
public void f() {
super.f();
value = 200;
System.out.println("ChildClass Value:"+value);
System.out.println("父类value:"+super.value);
}
}
public class demo {
public static void main(String[] args) {
ChildClass cc = new ChildClass();
cc.f();
}
}
程序执行完成后,内存中的图解如下:
super关键字
继承中的构造方法
1.子类的构造过程中必须调用其基类的构造方法。
2.子类可以在自己的构造方法中使用super(argument_list)调用基类的构造方法。(使用this(argument_list)调用本类中另外的构造方法)
3.如果子类的构造方法中没有显示的调用基类构造方法,则系统默认调用基类无参数的构造方法。
4.如果子类构造方法中既没有显示调用基类构造方法,而基类中又没有无参的构造方法,则编译出错。
5.如果调用super,必须写在子类构造方法的第一行。
Object类
Object类是Java类的根基类,如果在类的声明中未使用extends关键字指明其基类,则默认基类为Object类。
toString():1.描述当前对象的有关信息。
2.在进行String与其他类型数据的连接操作时,如"value :" + value,将自动调用该对象类的toString()方法。
3.toString()可重写。
equals方法
Api中是这样解释Object类中的equals方法的:
指示其他某个对象是否与此对象“相等”。
equals 方法在非空对象引用上实现相等关系:
1. 自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
2. 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
3. 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
4. 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
5.对于任何非空引用值 x,x.equals(null) 都应返回 false。
Object 类的 equals 方法实现对象上差别可能性最大的相等关系;
即,对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,
此方法才返回 true(x == y 具有值 true)。
注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,
该协定声明相等对象必须具有相等的哈希码。
总结下来就是:
1.Object类的equals方法定义为:x.equals(y)当x和y是同一个对象的引用时返回true,否则返回false。(对比:与“ == ” 相同)
2.Java中的某些类,如String,Date等,重写了Object的equals方法,调用这些类的equals方法,x.equals(y),当x和y所引用的对象是同一类对象且属性内容相等时返回true,否则返回false。对于各个类重写的equals()可翻看源码查看具体比较的是什么。
3.equals()可重写。
hashCode()
对于hashCode在Api中的解释如下:
hashCode 的常规协定是:
* 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,
必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。
从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
* 如果根据equals(Object) 方法,两个对象是相等的,
那么对这两个对象中的每个对象调用 hashCode方法都必须生成相同的整数结果。
* 如果根据equals(java.lang.Object)方法,两个对象不相等,那么对这两个对象中的任一对象上调用
hashCode方法不要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。
(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
在进行两个对象比较时,有时使用equals()有时使用hashCode(),两个相等的对象必须具有相同的hashcode值,也就是所谓的地址(不是纯粹的物理地址)。在下一章容器中会讲到,当比较Map中的键时,一般使用hashCode()来比较,hashCode()效率更高。
哈希编码: 在内存中的每个对象,都是通过它自己独有的哈希编码找到的。在栈内存中存储的,也就是我们前面所说的指向对象的地址。
对象转型
1.一个父类的引用类型变量可以指向其子类对象。
2.一个父类引用不可以访问其子类对象新增加的成员(属性和方法)
3.可以使用 变量 instanceof 类名 来判断该引用型变量所指向的对象是否属于该类或者该类的子类。
4.子类的对象可以当作父类的对象来使用,称作向上转型,反之称作向下转型。
父类对象的引用指向子类对象,叫做向上转型。反之叫做向下转型。
看这样一个小程序:
class Animal{
public String name;
Animal(String name) {
this.name = name;
}
}
class Cat extends Animal{
public String eyesColor;
Cat(String n,String c) {
super(n);
eyesColor = c;
}
}
class Dog extends Animal{
public String furColor;
Dog(String n,String c) {
super(n);
furColor = c;
}
}
public class Test1 {
public static void main(String[] args) {
Animal a = new Animal("name");
Cat c = new Cat("catName","blue");
Dog d = new Dog("dogName","black");
System.out.println(a instanceof Animal);
System.out.println(c instanceof Animal);
System.out.println(d instanceof Animal);
System.out.println(a instanceof Cat);
a = new Dog("bigYellow","yellow");
System.out.println(a.name);
System.out.println(a instanceof Animal);
System.out.println(a instanceof Dog);
Dog d1 = (Dog)a;
System.out.println(d1.furColor);
}
}
当程序运行到a = new Dog("bigYellow","yellow"); 时,此时父类引用指向了子类的对象,在内存中是这样的:
向上转型
也可以这样理解:a只看得到Dog中的Animal部分,当把a强制转换成Dog类型之后,a才能看到整个Dog部分,才可以访问到Dog中的furColor属性。
多态(动态绑定)
动态绑定是指在执行期间(而非编译期间)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
看一段代码:
class Animal{
private String name;
Animal(String name){
this.name = name;
}
public void enjoy(){
System.out.println("roal......");
}
}
class Cat extends Animal{
private String eyesColor;
Cat(String name,String eyesColor){
super(name);
this.eyesColor = eyesColor;
}
public void enjoy(){
System.out.println("cat roal.....");
}
}
class Dog extends Animal{
private String furColor;
Dog(String name,String furColor){
super(name);
this.furColor = furColor;
}
public void enjoy(){
System.out.println("dog roal......");
}
}
class Lady{
private String name;
private Animal pet;
Lady(String name,Animal pet){
this.name = name;
this.pet = pet;
}
public void myPetEnjoy(){
pet.enjoy();
}
}
public class Test1{
public static void main(String[] args) {
Cat c = new Cat("catName","blue");
Dog d = new Dog("dogName","black");
Lady l1 = new Lady("l1",c);
Lady l2 = new Lady("l2",d);
l1.myPetEnjoy();
l2.myPetEnjoy();
}
}
打印出的结果为:
图片.png
l1中的pet,new出的实际是Cat类型的对象,而l2中的pet,new出的实际是Dog类型的对象,所以调用enjoy()分别是Cat中的enjoy和Dog中的enjoy。具体内存图解如下:
图片.png
图片.png
所谓动态绑定,就是运行时new出什么对象,就调用该对象中的方法。
实现多态需要有三个条件:
1.要有继承。
2.要有重写。
3.父类引用指向子类对象。
当这三个条件满足之后,当调用父类中被重写的方法时,实际中new的是哪个子类对象,就调用哪个子类对象中的该方法。
抽象类
用absetract关键字修饰的类和方法分别叫做抽象类和抽象方法。含有抽象方法的类必须被声明为抽象类,抽象类必须被继承,抽象方法必须被重写。抽象类不能被实例化,抽象方法只需声明,不需实现。
final关键字
final可以用来修饰变量,方法,类。用final修饰的变量值不能被改变(可以理解成final的值是只读的),方法不能被重写,类不能被继承。
接口(interface)
接口是抽象方法和常量值的定义的集合。
从本质上讲,接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义。
public interface Runner {
public static final int id = 1;
//等同于这句话
int id = 1;
void start();
void run();
void stop();
}
接口的特性:
1.接口可以多重实现。
2.接口中声明的属性默认为public static flnal的,也只能是public static flnal的。
3.接口中只能定义抽象方法,而且这些方法默认为public的,也只能是public的。
4.接口可以继承其它接口,并添加新的属性和抽象方法。
看下面这段小程序:
interface Singer{
void sing();
void sleep();
}
interface Printer{
void paint();
void eat();
}
class Student implements Singer{
private String name;
Student(String name){
this.name = name;
}
@Override
public void sing() {
System.out.println("student is singing......");
}
/**
* @return the name
*/
public String getName() {
return name;
}
public void study() {
System.out.println("Studying....");
}
@Override
public void sleep() {
System.out.println("student is sleeping.....");
}
}
class Teacher implements Singer,Printer{
private String name;
/**
* @return the name
*/
public String getName() {
return name;
}
Teacher(String name){
this.name = name;
}
public void teach() {
System.out.println("teaching");
}
@Override
public void sing() {
System.out.println("teacher is singing....");
}
@Override
public void sleep() {
System.out.println("teacher is sleeping.....");
}
@Override
public void paint() {
System.out.println("teacher is painting......");
}
@Override
public void eat() {
System.out.println("teacher is eating....");
}
}
/**
* Test
*/
public class Test {
public static void main(String[] args) {
Singer s1 = new Student("le");
s1.sing();
s1.sleep();
Singer s2 = new Teacher("steven");
s2.sing();
s2.sleep();
Printer p1 = (Printer)s2;
p1.paint();
p1.eat();
}
}
对这句代码的解释:
Singer s1 = new Student("le");
s1.sing();
s1.sleep();
图片.png
图片.png
第二句:
Singer s2 = new Teacher("steven");
s2.sing();
s2.sleep();
Printer p1 = (Printer)s2;
p1.paint();
p1.eat();
这段代码的前半部分跟上面是相同的,s2只能看到sing()和sleep(),调用的是Teacher的sing()和sleep(),当s2被强制转化成Printer对象之后,只看得到Printer中的方法,而此时new出的是Teacher对象,调用的就是Teacher的paint()和eat().
至此,面向对象的知识点基本上是过完了。
下一章,讲异常。
易出错问题总结:
1.形参列表在内存中的分配情况:
形参也是一种局部变量,是专门为某个方法分配的局部变量,当方法执行时,在栈内存中临时分配一小块空间,用来存储它。当方法执行结束,为方法分配的局部变量在内存中消失,为其分配的这块内存被释放。构造方法的形参也是如此。
2.方法存在于内存中的代码区(code seg),方法名指向存储该段代码的代码区。
5.方法的返回值,也是在栈内存中临时创建一块内存,将返回的值赋值给它。
欢迎关注个人公众号,加入进来一起学习吧!
平头哥写代码