(二)面向对象基础
2.1类与对象
2.1.1 类与对象的内存分析
类属于引用数据类型,所以定义类需要内存的开辟和使用,new关键字主要的功能就是开辟内存空间。
重点 内存空间的概念:
堆内存:堆内存中保存的是对象的真正数据,都是每一个对象的属性内容 要想开辟堆内存空间,就要new关键字
栈内存:栈内存保存的是一块堆内存的空间地址。
public class Demo(){
public static void main(String args[]){
Book bk=null; #声明对象
bk=new Book() #创建对象
bk.title="java"
}
}
如上面代码,声明对象后,栈空间内存在一个bk(地址),在创建对象时才会进入到堆内存存放对象的属性,之后给对象属性赋值。
如果使用了没有实例化的对象,直接给对象属性赋值,会报错:NullPointerException 空指向异常。原因就是没有为其开辟堆内存空间。
由此,以一个问题继续深入内存:==与equals()的区别?
1、等于关系操作符作用:
a:用于比较两个操作符的数值大小
b:用于比较两个基本数据类型的变量指向是否相同,String的直接引用类型是否指向同一个字符串。
不用于比较看似相同的两个对象,对象间的比较用equals()方法
// "=="操作符的例子
public static void main(String[] args) {
//基本类型数据,n1和n2在栈内存中都指向5(也在栈内存),可用==比较
int n1 = 5;
int n2 = 5;
System.out.println(n1 == n2);
//String引用类型,s1、s2都指向常量池中的“word”,可用==比较
String s1 = "word";
String s2 = "word";
System.out.println(s1 == s2);
//new对象的引用integer1和integer2在栈内存引用地址不同,在堆内存中是两个对象,不可用==比较
Integer integer1 = new Integer(3);
Integer integer2 = new Integer(3);
System.out.println(integer1 == integer2);
//String new的对象 在堆内存中是两个对象,比较不可用==比较
String s3 = new String("good");
String s4 = new String("good");
System.out.println(s3 == s4);
}
//输出:
true
true
false
false
2、equals的作用: equals()是Object 中的方法,用于判断两个变量是否是对同一个对象的引用,即堆中的内容是否相同。
// equals()的例子
public static void main(String[] args) {
Integer integer3 = new Integer(3);
Integer integer4 = new Integer(3);
System.out.println(integer3.equals(integer4));
//String new的对象 在堆内存中是两个对象,比较不可用==比较
String s5 = new String("good");
String s6 = new String("good");
System.out.println(s5.equals(s6));
}
//输出:
true
true
2.2.2引用数据的初步分析
1、对象的引用传递
引用传递的核心在于:一块堆内存空间可以被多个栈内存空间所指向,每一个栈内存都可以修改同一块堆内存空间的属性值。
public class Book {
String title;
double price;
}
class TestDemo {
public static void main(String[] args) {
Book bookA=new Book();
Book bookB=null;
bookA.title="j";
bookA.price=12;
bookB=bookA;
bookB.price=194;
System.out.print(bookA.price);
}
}
输出:194
}
两个栈内存指向了同一个堆内存,所以改变任何一个属性,另一个属性也会发生变化
深入了解
如图所示,当实例化两个对象时,其栈内存分别指向不同的堆内存,但是发生引用传递时,一个栈内存指向另一个对象的堆内存,这时就会产生垃圾。一块没有栈内存指向的堆内存空间就是垃圾,这就需要垃圾回收器来进行回收
2、关于GC垃圾回收器处理。
GC垃圾回收器决定了内存的分配。当开发者创建一个对象后,GC就会监视这个对象的地址,大小和状态。对象的引用会保存在栈内存中,对象的具体内容会保存在堆内存中,当GC检测到一个堆中的某个对象不再被栈所引用,就会这个对进行不定期的回收。
垃圾处理方法:
- 引用计数:一个实例化对象,如果由程序使用了这个引用对象,引用计数+1,当一个独享使用完毕引用计数-1.引用计数为0时回收
- 跟踪收集:从root set (包括当前正在执行的线程、全局或者静态变量、JVM Handles、JNDI Handles )开始扫描有引用的对象,如果某个对象不可到达,则说明这个对象已经死亡(dead),则GC可以对其进行回收。也就是说:如果A对象引用了B对象的内存,那么虚拟机会记住这个引用路径,而如果一一个对象没有在路径图中,则就会被回收。
- 基于对象跟踪的分代增量收集:所有的对象回收要根据堆内存的结构划分来进行收集,具体如下
①基于对象跟踪:是由跟踪收集发展而来的,分代是指对堆进行了合理的划分,JVM将整个堆分为以下三代。
A. YoungGen (新生代,使用Minor GC回收): YoungGcen 区里面的对象的生命周期比较短,GC对这些对象进行回收的时候采用复制拷贝算法。
young: 又分为eden、survivorl ( from space)、survivort;(tosapce)。eden是在每个对象创建的时候才会分配的空间,当eden无法分配时,则会自动触发一次MinorGC。当GC每次执行时,都会将eden空间中存活的对象和survivor1中的对象拷贝到survivor2中,此时eden和survivor1的空间将被清空。当GC执行下次回收时将eden和survivor2中的对象拷贝到survivor1中,同时会清空eden和survivor2空间。按照此类顺序一次执行,经过数次回收将依然存活的对象复制到OldGen中。
OldGen (年老代)区。
B. OldGen(年老代, 使用Major GC回收):当对象从YoungGen 保存到OldGen后,会检测OldGen的剩余空间是否大于要晋升对象的大小,此时会有以下两种处理形式。
1-如果小于要保存的对象,则直接进行-次 Full GC (对整个堆进行扫描和回收,但是Major GC除外),这样就可以让OldGen腾出更多的空间。然后执行MinorGC,把YoungGen空间的对象复制到OldGen空间
1-如果大于要保存的对象,则会根据条件( HandlePromotionFailure配置:是否允许担保分配内存失败,即整个OldGen空间不足,而YoungGen空间中Eden和Survivor对象都存活的极端情况。)进行MinorGC和FullGC回收。
C. PermGen (持久区):要存放加载进来的类信息,包括方法、属性、对象池等,满了之后可能会引起Out of MEmory错误。
MetaSpace (元空间):持久化的替换者,直接使用主机内存进行存储。
②增量收集:不是每一-次都全部收集,而是累积的增量收集。
2.2封装性分析
在类中定义的属性都要求使用private声明,如果属性需要被外部所使用,你那么按照要求定义相应的getter和setter方法
2.3构造方法
- 构造方法和普通方法的区别:
(1)调用时机不同,构造方法时在实例化新对象的时候只调用一次
普通方法时实例化对象产生后,通过对象名.方法名调用多次。
构造方法的核心作用:在类对象实例化时设置属性的初始化内容。 - 关于属性默认值问题:
在定义一个类时,可以指定它的默认值,但是在在构造没有执行之前,这个默认值才会设置。
class Book{
private String title="Java 开发"
public Book(){} //构造方法
}
在构造完成后,才会将java开发这个内容设置给Title
2.4匿名对象
匿名对象就是没有栈内存指向堆内存空间,这个对象智能使用一次,使用完就回收
public static void main(String[] args) {
new Book("java",69.8) //匿名对象的建立
}
2.5数组
- 数组与方法参数的传递
public class list {
public static void main(String[] args) {
int data[]=new int[] {1,2,3};
change(data);
for(int x=0;x<data.length;x++)
System.out.print(data[x]+"、");
}
public static void change(int temp[]) {
for(int x=0;x<temp.length;x++) {
temp[x]*=2;
}
}
}
这里将数组传给方法中的参数,这就是数组的引用传递
数组的排序、数组的转置
2. 数组的操作
(1)数组复制
System.arraycopy(源数组名称,源数组开始复制索引,目标数组名称,目标数组开始索引,长度)
(2)数组排序
java.util.Arrays.sort(数组)
3. 对象数组
class Book(){
private double price;
}
public class Demo(){
public static void main(String args[]){
//动态实例化
Book bookA[]=new Book[3]; //开辟对象数组空间
bookA[0]=new Book[50];//对对象数组进行实例化
//静态实例化
Book bookB[]=new Book[]{
new Book(60);
}
}
}
2.6String类
2.6.1字符串的比较
通过==只能比较的是数值
(1)采用构造方法实例化字符串
public static void main(Stirng arg[]){
String stra="hello"; //直接赋值
String strb=new String("hello"); //构造方法定义
String strc=strb; //引用传递
System.out.print(stra==strb); //false
System.out.print(strc==strb);//true
}
从上述输出可以看出,==只能比较数值的大小(内存地址),而stra与strb用=比较的数值是它的地址的数值,而地址不同,通过引用传递后strc的地址与strb是相同的,所以strc=strb
只要是引用数据类型就会存在内存地址,用=比较的就是其内存地址。
如果要比较引用数据类型的内容,需要用equals()
(2)采用直接赋值实例化字符串
public static void main(Stirng arg[]){
String stra="hello";
String strb="hello";
String strc="helloworld";
System.out.print(stra==strb); //ture
System.out.print(strc==strb);//false
}
采用直接赋值实例化字符串时,由于设置的内容相同,所以即使没有发生对象引用,最终的三个String对象也指向同一个堆内存空间
String两种实例化方法的区别和缺点
直接赋值(String str=“hello”):只会开辟一块堆内存空间,并且会保存在对象池中以便于下次使用。
构造方法(String str=new String(“hello”)):会开辟两块内存空间,(首先在堆内存中开辟空间保存hello,然后使用关键词new 开辟另一块堆空间。真正使用的是new开辟的空间,而之前开辟的空间会成为垃圾等待回收。)不会自动入池,但是可以通过intern()方法手工入池
public static void main(Stirng arg[]){
String stra="hello";
String strb=new String("hello");
strb.intern();//手工入池
System.out.print(stra==strb);//true
(3)避免空指针异常
public static void main(Stirng arg[]){
String input=null; //直接赋值
if(input.equals("hello")){
System.out.print("hello");
} //会产生空指针异常,因为input的内容是空 栈内存没有指向堆内存
if("hello".equals(input)){
System.out.print("hello");
} //因为字符串常量是个String类的匿名对象所以该对象不会是NULL,所以将不会出现空指针异常
}
2.6.2字符串一旦被定义则不可改变
在进行String类对象内容修改时,实际上原视的字符串都没有发生变化,改变的只是String类对象的引用关系(最终没有被引用的堆空间都将成为垃圾空间)
public static void main(Stirng arg[]){
String str="Hello";
str+="World";
str+="!!!"
}
所以在开发的过程中,尽量不要在循环语句中不断修改字符串,这样会产生很多的垃圾空间。
如果在开发中需要频繁的修改该如何解决?
回答:使用StringBuffer或StringBuilder类代替
2.6.3字符、字节与字符串的操作
(1)取指定索引字符-----charAt()方法
(2)字符数组与字符串的转换----toCharArray() String(char[] value,int offset,int count)
(3)字节数组与字符串的转换----getBytes() String(byte[]value,int offset,int count)
public static void main(Stirng arg[]){
String str="Hello";//将大写转小写,分别使用字符串转字符数组和字符串转字节数组
char []data=str.toCharArray();
for(int x=0;x<data.length;x++){
data[x]-=32;
}
System.out.println(String(data)); //转为字符串
byte []data2=str.getByte();
for(int x=0;x<data.length;x++){
data[x]-=32;
}
System.out.println(String(data));
2.6.4字符串的查找
方法的使用如下面代码
public static void main(Stirng arg[]){
String str="helloworld!:";
System.out.println(str.contain("h")); //true
System.out.println(str.indenxOf("l")); //2
System.out.println(str.indexOf("l",4));//-1
}
2.6.5字符串的替换
1、public String replaceAll(String regex,String replacement) 用新的替换所有旧的内容
2、public String replaceFirst(String regex,String replacement) 替换首个满足条件的内容
2.6.6字符串的截取
1、public String substring(int beginIndex) 从指定索引处截取到结尾
2、public String substring(int beginIndex,int endIndex) 从指定开始截取到指定结束
注意:参数一定要是正整数
2.6.7字符串的拆分
1、public String[] split(String regex) 按照后面指定字符进行全部拆分
2、public String[] split(String regex,int limit) 部分拆分,最后数组长度由limit决定
注意:为了避免正则表达式的影响,对某些要进行转义
public static void main(Stirng arg[]){
String str="张三:20|李四:21";
String result[]=str.split("\\|"); //转义
for (int x=0;x<result.length;x++){
String temp[]=result.split(":");
System.out.println("姓名"+temp[0]+"年龄"+temp[1]);
}
}
2.7this关键字
this关键字的作用:
1、调用本类属性
2、调用本类方法
3、表示当前对象
2.7.1调用本类属性
在一些程序中,可能会出现方法参数名和属性名称重复的情况,这时就要使用this调用本类属性。
class Book(){
private String title;
private double price;
public Book(String title,double price){
//这里有一个两参数的构造方法,对对象进行实例化时,调用两参构造方法
this.title=title;//title=title是错误的 title是构造方法中的参数
this.price=price;
}
public class test(){
public static void main(Stirng arg[]){
Book book=new Book("javakaifa",125.4);
System.out.println(book.title)
}
}
}
上述代码运行结果会显示书名和价格分别是null和0.0。
2.7.2调用本类方法
减少冗余代码
class Book {
private String title;
private double price;
public Book(){
System.out.println("需要执行50次");
}
public Book(String title){
this(); //执行一个参数的构造方法调用无参构造,这样就不用在一个参数的构造方法中写重复的语句了
this.title=title;
}
public Book(String title,double price){
this(title);
this.title=title;
this.price=price;
}
}
public class TestDemo{
public static void main(String args[]){
Book book=new Book("javakaifa",84);
}
}
注意不能再构造方法中出现嵌套的现象,就是无参调用一个参数,一个参数调用两个参数,两个参数调用无参,形成一个循环,这样就会报“构造方法递归调用”
2.7.3表示当前对象
2.8引用传递
引用传递的应用
class Person{
private String name;
private Car car;
private Person child;
public Person(String name){
this.name=name;
}
public void setName(String name){
this.name=name;
}
public String getName() {
return name;
}
public void setCar(Car car) {
this.car = car;
}
public Car getCar() {
return car;
}
public void setChild(Person child) {
this.child = child;
}
public Person getChild() {
return child;
}
public String getInfo(){
return "人员名称"+this.name;
}
}
class Car{
private String cname;
private Person person;
public Car(String cname){
this.cname=cname;
}
public void setCname(String cname) {
this.cname = cname;
}
public String getCname() {
return cname;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
public String getInfo(){
return "车名称"+this.cname;
}
}
public class TestDemo2 {
public static void main(String args[]){
Person p1=new Person("李华");
Person child1=new Person("李小华");
Car c1=new Car("奥拓");
Car c2=new Car("劳斯莱斯");
p1.setCar(c1);
c1.setPerson(p1);
child1.setCar(c2);
c2.setPerson(child1);
p1.setChild(child1);
System.out.println(p1.getCar().getInfo());
System.out.println(c1.getPerson().getInfo());
System.out.println(p1.getChild().getInfo());
System.out.println(p1.getChild().getCar().getInfo());
}
}
2.9static关键字
static可以用来定义属性和方法
静态变量是随着类加载时被完成初始化的,它在内存中仅有一个,且JVM也只会为它分配一次内存,同时类所有的实例都共享静态变量,可以直接通过类名来访问它。
但是实例变量则不同,它是伴随着实例的,每创建一个实例就会产生一个实例变量,它与该实例同生共死。
所以我们一般在这两种情况下使用静态变量:对象之间共享数据、访问方便。
2.9.1static定义属性
static定义的属性是公有属性,可以由对象调用,也可以由类直接调用。调用之后改变其值。
常用的内存区域:
全局数据区:保存static类型的属性
全局代码区:保存所有的方法定义
static定义的属性和普通属性不同点:
(1)static定义的属性是公有属性,可以由对象调用,也可以由类直接调用。
(2)所有非static属性必须产生实例化对象才可以访问,而static属性不受实例化对象的控制。
2.9.2static定义方法
注意:
static方法不能直接访问非static属性或方法,只能调用static属性或方法,如果想要调用非static属性或方法,必须创建对象来调用
public class TestDemo5 {
public static void main(String args[]){
new TestDemo5().fun(); //fun()
}
public void fun(){//public static void fun()
System.out.println("nihaoshijie");
}
}
上述代码中 main函数是static方法,fun是非static方法 调用fun只能通过对象来调用。或者将fun改为static方法,可以直接调用
关于static方法访问限制的说明:
(1)所有的非static定义的结构,必须在类已经明确产生实例化对象时才会分配对空间,才能使用
(2)所有static定义的结构,不受实例化对象的控制,即可以在没有实例化对象的时候访问。
2.9.3static的应用
1、static计数
class Student{
private static int num=0;
public Student(){
System.out.println(num+",");
num++;
System.out.println("number"+num+",");
}
}
public class TestDemo6 {
public static void main(String args[]) {
new Student();
new Student();
}
}
如果num不加static 每次创建对象时 num值又变回了0
2.10内部类
定义内部类
//定义内部类 通过外部类的fun方法,访问外部类的私有属性
class outer1{ //外部类内部定义内部类
private String msg="hello world";
class inner1{
public void print(){
System.out.println(msg);
}
}
public void fun(){
new inner1().print();
}
}
class outer2{//外部类外部定义内部类
private String msg="hello world";
public void fun(){
new inner2(this).print();//this表示当前调用fun()方法的对象 主方法中由out对象调用,所以这里this就是out
}
public String getMsg(){
return this.msg;
}
}
class inner2{
private outer2 outer2;
public inner2(outer2 outer2){
this.outer2=outer2;
}
public void print(){
System.out.println(this.outer2.getMsg());
}
}
//外部类访问内部类的私有属性
class outer3{
private String msg="hello world";
class inner3{
private String info="hello world";
public void print(){
System.out.println(msg);
}
}
public void fun(){
inner3 inner3=new inner3();
System.out.println(inner3.info);
}
}
public class TestDemo7 {
//outer1
public static void main(String args[]){
outer1 out1=new outer1();
out1.fun();
outer2 outer2=new outer2();
outer2.fun();
outer3 outer3=new outer3();
outer3.fun();
}
}
如何采用普通对象那样直接在外部直接产生实例化对象呢?
外部类.内部类 对象=new 外部类().new 内部类()
2.10.1使用static定义内部类
使用static定义的内部类,在创建内部类实例化对象时
外部类.内部类 对象=new 外部类(). 内部类()
class outer4{ //外部类内部定义内部类
private static String msg="hello world";
static class inner4{
public void print(){
System.out.println(outer4.msg);
}
}
}
public class TestDemo8 {
public static void main(String args[]){
outer4.inner4 inner4=new outer4.inner4();
inner4.print();
}
}
2.10.2在方法中定义内部类
在方法中定义内部类,在jdk1.8之后可以直接访问参数和变量
注意:在1.8之前,方法中定义内部类如果想要访问方法的参数或者方法定义的变量,在参数和方法之前一定要加上final标记
//在方法中定义内部类 分别访问外部类的属性、方法的参数、方法中定义的变量
class outer5{
private String msg="hello world";
public void fun(int num){ //final int num
double score=99.9; //final double score
class inner5{
public void print(){
System.out.println("外部类的属性:"+outer5.this.msg);
System.out.println("方法参数:"+num);
System.out.println("方法变量:"+score);
}
}
new inner5().print(); //内部类实例化并输出
}
}
public class TestDemo9 {
public static void main(String args[]){
outer5 outer5=new outer5();
outer5.fun(64);
}
}