面向对象基础 目录
面向对象、类与对象
面向对象的基本概念
程序的发展主要经历了,面向过程和面向对象编程。
- 二者的差异是,面向对象在动手之前对整体有一个设计和分析的过程,需要的东西都提前做准备和设计。面向过程则是直接动手做,过程中需要什么工具随取随用。
这在制作过程中,如果想要修改,那么面向对象会非常清晰,非常好修改,但是面向过程的话,就会不是特别好修改。
- 面向对象对现实世界有一个抽象的过程,相比面向过程多了一些分析方法,设计和分析过程。
C语言就是面向过程的,C++、java就是面向对象的。
面向对象的3个主要特征
- 封装 encapsulate
-对外部不可见
封装性:封装性就是对外部不可见,起到保护程序的某些内容,但是不能封装的死死的,得向外提供接口。 - 继承 inheritance
-扩展类的功能
继承性:扩展类的功能。 - 多态 polymorphism
-方法重载
-对象多态
多态性:方法名相同,但是方法的参数类型或者个数不同就是方法的重载,是一种多态。还有对象的多态性。
3大特性都是为了在程序设计中达到解耦合的目的。
类与对象的关系、类的定义、对象的声明、创建及使用
整个面向对象中,最重要的是类与对象,因为面向对象的核心组成就是类与对象。
什么是类?什么是对象?
类对某一类事物的描述,是从具体对象中抽象出来的,是抽象的,是概念上的定义。
对象是实际存在的该类对象的具体的一个个体。也称为实例(instance)。
类相当于是一个模板,依照模板产生具体的产品。
使用关键字class
即可定义一个类。
一个类定义之后不能直接使用,需要产生对象。
类名 对象名称 = null;
对象名称 = new 类名();
类名 对象名称 = new 类名();
java中的内存划分、引用传递、垃圾产生
类和数组一样属于引用数据类型,引用数据类型肯定存在栈内存到堆内存的引用关系。实际上在类与对象的关系中,也存在这样的引用关系。
Person per = new Person();
声明对象: Person per=null;
Person per表示声明对象,对象的声明是在栈内存中声明的,与数组一样数组名称是保存在栈内存之中。
只开辟了栈内存的对象是无法使用的,必须有其堆内存的引用才可以使用。
实例化对象: new Person();
在堆中开辟空间,对象实例化后,其所有的属性的内容都是默认值。
对象保存在栈内存之中,
属性保存在堆内存之中,
方法保存在方法区,该区域是所有对象共享的。
注意点:
1,在使用对象的时候,对象必须被实例化之后才可以使用(实例化对象,并不单单指直接使用new关键字实现,只要其有堆内存的空间指向,则就表示实例化成功)。
2,如果不实例化,直接使用对象,会报NullPointerException
空指针的空指向异常。
3,在引用操作中,如果一个对象没有堆内存的引用,而调用了类中的属性或方法,就会出现该问题。
4,所谓引用传递,实际上传递的就是堆内存的使用权,可以为一个堆内存空间定义多个栈内存的引用操作。
class Person{
String name ; // 声明姓名属性
int age ; // 声明年龄属性
public void tell(){ // 取得信息
System.out.println("姓名:" + name + ",年龄:" + age) ;
}
};
public class ClassDemo06{
public static void main(String args[]){
Person per1 = null ; // 声明per1对象
Person per2 = null ; // 声明per2对象
per1 = new Person() ; // 实例化per1对象
per2 = new Person() ; // 实例化per2对象
per1.name = "张三" ; // 设置per1中的name属性内容
per1.age = 30 ; // 设置per1中的age属性内容
per2.name = "李四" ;
per2.age = 33 ; // 设置per2中的age属性内容
per2 = per1 ; // 把per1的堆内存空间使用权给per2
System.out.print("per1对象中的内容 --> ") ;
per1.tell() ; // 调用类中的方法
System.out.print("per2对象中的内容 --> ") ;
per2.tell() ; // 调用类中的方法
}
};
因为per2本身有堆内存的空间指向,所以如果要想再指向per1对应的空间,则必须先断开已有的链接。
per2=per1;
之后,per2原本的空间指向的堆内存没有了任何栈内存空间所引用了,就成了垃圾空间,等待垃圾回收机制进行回收。
因为per2改变了指向,所以其原本的堆内存空间就没有了任何栈的引用,则这样的空间就称为垃圾,等待垃圾回收机制回收。
垃圾回收机制简称GC。
总结:
1,栈与堆内存的关系;
2,对象保存在栈内存之中,而具体的内容保存在堆内存之中。
3,对象的引用传递,实际上传递的就是堆内存空间的使用权。
4,垃圾的产生。
内存操作:为属性赋值-----封装性
封装产生的目的
封装就是保护内容。
保证某些属性、方法不被外部看见。
看一下代码:
class Person{
String name ; // 声明姓名属性
int age ; // 声明年龄属性
public void tell(){
System.out.println("姓名:" + name + ",年龄:" + age) ;
}
};
public class EncDemo01{
public static void main(String arsgh[]){
Person per = new Person() ; // 声明并实例化对象
per.name = "张三" ; // 为name属性赋值
per.age = -30 ; // 为age属性赋值
per.tell() ;
}
};
从语法角度看,没有任何问题,但是从实际角度看,年龄是个负数,明显不合理,那么以上的代码如何修改最合理?对年龄的验证放在哪里最合理,是对象的定义的Person类中还是对象的使用的类EncDemo01中?这是个好问题!!!
首次改进代码:—属性封装起来。java中使用private关键字实现属性、方法的封装。
如下:
class Person{
private String name ; // 声明姓名属性
private int age ; // 声明年龄属性
public void tell(){
System.out.println("姓名:" + name + ",年龄:" + age) ;
}
};
public class EncDemo02{
public static void main(String arsgh[]){
Person per = new Person() ; // 声明并实例化对象
per.name = "张三" ; // 为name属性赋值
per.age = -30 ; // 为age属性赋值
per.tell() ;
}
};
运行程序发现,属性封装的死死的了,外部无法调用到了,继续优化代码。
二次优化:----给封装的死死的属性向外提供访问接口,setter、getter方法。
如下:
class Person{
private String name ; // 声明姓名属性
private int age ; // 声明年龄属性
public void setName(String n){ // 设置姓名
name = n ;
}
public void setAge(int a){ // 设置年龄
age = a ;
}
public String getName(){ // 取得姓名
return name ;
}
public int getAge(){ // 取得年龄
return age ;
}
public void tell(){
System.out.println("姓名:" + name + ",年龄:" + age) ;
}
};
public class EncDemo03{
public static void main(String arsgh[]){
Person per = new Person() ; // 声明并实例化对象
per.setName("张三") ; // 调用setter设置姓名
per.setAge(-30) ; // 调用setter设置年龄
per.tell() ; // 输出信息
}
};
OK,至此也做了封装了,语法不报错了,
仔细想想在哪里做对年龄合法化的判断,其实应该加载类本身,如果年龄不合理则就不应该为属性赋值,类本身就应该就给提供良好的对象,而不是对象生成以后再在外面给对象做判断和限制。
所以,属性的合理性控制的判断,需要加载在Person类的定义中,并且是在向外提供的setter方法中。
三次优化:------setter中增加合理性判断。
class Person{
private String name ; // 声明姓名属性
private int age ; // 声明年龄属性
public void setName(String n){ // 设置姓名
name = n ;
}
public void setAge(int a){ // 设置年龄
if(a>=0&&a<=150){ // 加入验证
age = a ;
}
}
public String getName(){ // 取得姓名
return name ;
}
public int getAge(){ // 取得年龄
return age ;
}
public void tell(){
System.out.println("姓名:" + name + ",年龄:" + age) ;
}
};
public class EncDemo04{
public static void main(String arsgh[]){
Person per = new Person() ; // 声明并实例化对象
per.setName("张三") ; // 调用setter设置姓名
per.setAge(-30) ; // 调用setter设置年龄
per.tell() ; // 输出信息
}
};
tell 方法中调用了属性,为了调用对象明确起见,我们最好给属性明确指出调用的是本类对象的属性,所以需要加上this关键字。this关键字表示当前对象。
封装的实现
1,private关键字。
2,setter、getter。
访问封装的内容
setter、getter方法的定义
构造方法、匿名对象
构造方法的概念以及调用时机
对象的产生格式:
类名称 对象名称 = new 类名称();
这里的类名称()
就是构造方法。一有对象产生,则就会调用构造方法。声明对象并不会调用构造方法,只有实例化对象的时候才回去调用构造方法。
class Person{
public Person(){ // 声明构造方法
System.out.println("一个新的Person对象产生。") ;
}
};
public class ConsDemo01{
public static void main(String args[]){
System.out.println("声明对象:Person per = null ;") ;
Person per = null ; // 声明对象时并不去调用构造方法
System.out.println("实例化对象:per = new Person() ;") ;
per = new Person() ;//实例化对象
}
};
1,构造方法的名称必须与类名称一致。
2,构造方法的声明处不能有任何返回值类型的声明。
3,不能在构造方法中使用return返回一个值。
问题:写一个类,没有明确的去定义一个构造方法,这个类为什么还是可以使用呢?
因为java的操作机制中,如果一个类中如果没有明确的声明一个构造方法,则会自动生成一个无参的什么都不做的构造方法供使用。保证类中至少有一个构造方法。
1,每个类中肯定都会至少有一个构造方法。
2,如果一个类中没有声明一个明确的构造方法,则会自动生成一个无参的什么都不做的构造方法。
3,但是,一旦明确的声明了一个构造方法,就不会自动生成这个无参的构造方法了。
class Person{
public Person(){} // 如果没有编写构造方法,则会自动生成此代码
}
构造方法的作用是为类中的属性初始化。
class Person{
private String name ;
private int age ;
public Person(String n,int a){ // 声明构造方法,为类中的属性初始化
this.setName(n) ;
this.setAge(a) ;
}
public void setName(String n){
name = n ;
}
public void setAge(int a){
if(a>0&&a<150){
age = a ;
}
}
public String getName(){
return name ;
}
public int getAge(){
return age ;
}
public void tell(){
System.out.println("姓名:" + this.getName() + ";年龄:" + this.getAge()) ;
}
};
public class ConsDemo02{
public static void main(String args[]){
System.out.println("声明对象:Person per = null ;") ;
Person per = null ; // 声明对象时并不去调用构造方法
System.out.println("实例化对象:per = new Person() ;") ;
per = new Person("张三",30) ;//实例化对象
per.tell() ;
}
};
根据以上代码,再次强调:构造方法的作用是为类中的属性初始化。
构造方法重载
构造方法与普通方法一样是支持重载操作的。只要方法的类型或者参数个数不同,则就可以完成重载操作。
class Person{
private String name ;
private int age ;
public Person(){} // 声明一个无参的构造方法
public Person(String n){ // 声明有一个参数的构造方法
this.setName(n) ;
}
public Person(String n,int a){ // 声明构造方法,为类中的属性初始化
this.setName(n) ;
this.setAge(a) ;
}
public void setName(String n){
name = n ;
}
public void setAge(int a){
if(a>0&&a<150){
age = a ;
}
}
public String getName(){
return name ;
}
public int getAge(){
return age ;
}
public void tell(){
System.out.println("姓名:" + this.getName() + ";年龄:" + this.getAge()) ;
}
};
public class ConsDemo03{
public static void main(String args[]){
System.out.println("声明对象:Person per = null ;") ;
Person per = null ; // 声明对象时并不去调用构造方法
System.out.println("实例化对象:per = new Person() ;") ;
per = new Person("张三",30) ;//实例化对象
per.tell() ;
}
};
匿名对象
匿名对象,就是没有名字的对象,在java中,如果一个对象只使用一次,则就可以将其定义成匿名对象。
class Person{
private String name ;
private int age ;
public Person(String n,int a){ // 声明构造方法,为类中的属性初始化
this.setName(n) ;
this.setAge(a) ;
}
public void setName(String n){
name = n ;
}
public void setAge(int a){
if(a>0&&a<150){
age = a ;
}
}
public String getName(){
return name ;
}
public int getAge(){
return age ;
}
public void tell(){
System.out.println("姓名:" + this.getName() + ";年龄:" + this.getAge()) ;
}
};
public class NonameDemo01{
public static void main(String args[]){
new Person("张三",30).tell() ; //这个new出来的对象就是匿名对象
}
};
使用了关键字new就表示开辟了堆内存空间,只有开辟了堆内存空间的对象才有意义。这个对象可以没有栈内存对象的指向,直接可以调用方法。这就是匿名对象。
所谓的匿名对象,就是比普通的对象少了一个栈内存的引用关系。
总结:
1,对象在实例化时,必须调用构造方法,每个类都有至少一个构造方法。
2,匿名对象是只开辟了堆内存的实例对象。
String类及其常用方法
String类的两种实例化方式及其区别
A:直接赋值
public class StringDemo01{
public static void main(String args[]){
String name = "jake" ; // 实例化String对象
System.out.println("姓名:" + name) ;
}
};
B:通过关键字new
public class StringDemo02{
public static void main(String args[]){
String name = new String("jake") ; // 实例化String对象
System.out.println("姓名:" + name) ;
}
};
两种实例化方式的区别、字符串的特征
两种实例化方式,使用哪种更合适?
要想解决这样的问题,则首先必须从字符串的特征说起。
字符串什么特征?:
字符串特征1:一个字符串就是一个String的匿名对象。
以下代码可以正确运行,就证明了,一个字符串就是一个String的匿名对象。
public class StringDemo06{
public static void main(String args[]){
System.out.println("hello".equals("hello")) ;
}
};
分析:
String name="jake"
对于这样的代码,就表示将一个堆内存空间的指向给了栈内存空间。
明确这些之后,再来分析,String的实例化使用哪种方式更合适的问题:
看如下代码,分析直接赋值时候的内存使用:
public class StringDemo07{
public static void main(String args[]){
String str1 = "hello" ; // 直接赋值
String str2 = "hello" ; // 直接赋值
String str3 = "hello" ; // 直接赋值
System.out.println("str1 == str2 --> " + (str1==str2)) ; // true
System.out.println("str1 == str3 --> " + (str1==str3)) ; // true
System.out.println("str2 == str3 --> " + (str2==str3)) ; // true
}
};
“==”比较的是地址,三个对象比较结果都是true,也就是三个内存地址是相同的,是同一个对象,三个栈内存引用指向的是同一个堆内存空间地址。
当创建相同的对象,java机制会去常量池去查找是否有创建过,如果已经创建过就不再重新创建,直接指向已有的地址,如果找不到才会去重新创建。这样可以有效的节省内存。
再分析使用new赋值时候的内存使用:
public class StringDemo08{
public static void main(String args[]){
String str1 = new String("hello") ;
}
};
使用了new关键字,肯定开辟了堆内存,实际使用的也是new开辟的空间里的对象,但是双引号里的值又是String的一个匿名对象,这个匿名对象是在常量池的,这个对象并不会被用到,就会被jc回收。
所以,至此得出:
使用直接赋值的方式只需要一个实例化对象即可,而使用new String的方式则意味着开辟了两个内存对象。
所以开发中,最好使用直接赋值的方式进行赋值。
字符串特征2:字符串的内容一经声明不可改变。
public class StringDemo09{
public static void main(String args[]){
String str = "hello" ; // 声明字符串
str = str + " world!!!" ; // 修改字符串
System.out.println("str = " + str) ;
}
};
此时,字符串的对象是改变了,但是字符串变了么?
分析一下:
首先,之前已经得出,一个双引号里的字符串,就是String的一个匿名对象。
分析内存:
实际上字符串内容的改变,改变的是内存地址的引用关系。原本的字符串并没有并改变。
所以开发中String尽量需要避免如下的使用:
public class StringDemo10{
public static void main(String args[]){
String str1 = "LiXingHua" ; // 声明字符串对象
for(int i=0;i<100;i++){ // 循环修改内容
str1 += i ; // 字符串的引用不断改变
}
System.out.println(str1) ;
}
};
这样的代码,没有语法问题,但是从内存角度分析,堆栈指向连接需要断开连接100次,这样的操作性能很低,应该避免使用。但是如果就是有这样的使用场景,需要在之前基础上改变字符串内容,则可以使用功能java常用类库中的StringBuilder
、StringBuffer
来完成。
String的两种比较操作
String的赋值有两种,比较也有两种。
使用 “==”进行String内存地址比较
对于基本数据类型来说,“ == ”比较的是值,但是对于String这种引用数据类型来说,“ == ” 比较的是内存地址。
public class StringDemo03{
public static void main(String args[]){
int x = 30 ;
int y = 30 ;
System.out.println("两个数字的比较结果:" + (x==y)) ;
}
};
public class StringDemo04{
public static void main(String args[]){
String str1 = "hello" ; // 直接赋值
String str2 = new String("hello") ; // 通过new赋值
String str3 = str2 ; // 传递引用
System.out.println("str1 == str2 --> " + (str1==str2)) ; // false
System.out.println("str1 == str3 --> " + (str1==str3)) ; // false
System.out.println("str2 == str3 --> " + (str2==str3)) ; // true
}
};
使用 equals() 进行值比较
String引用数据类型使用String类提供的 equals() 比较的是值。
public class StringDemo05{
public static void main(String args[]){
String str1 = "hello" ; // 直接赋值
String str2 = new String("hello") ; // 通过new赋值
String str3 = str2 ; // 传递引用
System.out.println("str1 equals str2 --> " + (str1.equals(str2))) ; // true
System.out.println("str1 equals str3 --> " + (str1.equals(str3))) ; // true
System.out.println("str2 equals str3 --> " + (str2.equals(str3))) ; // true
}
};
总结
1,String要使用直接赋值的方式,因为使用new会开辟两个空间,造成内存的浪费。
2,一个字符串就是String的一个匿名对象。
3,字符串的比较有两种方式,“== ”和equals()。