类就是对象操作的模板,但类不能直接使用,须通过实例化对象来使用
类本身属于引用数据类型
按照这种理解,可以得出如图所示内存关系:
如果要想开辟堆内存空间,只能依靠关键字new来进行开辟。即:只要看见了关键字new不管何种情况下,都表示要开辟新的堆内存空间。
引用传递是整个Java中的精髓所在,而引用传递的核心概念也只有一点:
一块堆内存空间(保存对象的属性信息)可以同时被多个栈内存共同指向,则每一个栈内存都可以修改同一块堆内存空间的属性值。
在所有的引用分析里面,最关键的还是关键字“new"。一定要注意的是,每一次使用关键字new定会开辟新的堆内存空间,所以如果在代码里面声明两个对象,并且使用了关键字new为两个对象分别进行对象的实例化操作,那么一定是各自占有各自的堆内存空间,并且不会互相影响。
public static void main(String arg[]) {
Book bookA = new Book();
Book bookB = new Book();
bookA.title = "Java";
bookA.price = 90;
bookB.title = "JSP"
bookB.price = 80;
bookB = bookA;
bookB.price = 100;
bookA.getInfo();
}
名称:Java,价格:100
本程序首先分别实例化 BOOKA与 BOOKB两个不同的对象,由于具休保存在不同的内存空间所以设置属性时不会互相影响。然后发生了引用传递(bookB= bookA),由于 bookB对象原本存在有指向的堆内存空间,并且一块栈内存只能够保存一块堆内存空间的地址,所以bookB要先断开已有的堆内存空间,再去指向 bookA对应的堆内存空间,这个时候由于原本的bookB堆内存没有了任何指向, bookB将成为垃圾空间。最后由于 bookB对象修改了 price属性的内容。程序的内存关系如图3所示。
通过内存分析可以发现,在引用数据类型关系时,一块没有任何栈内存指向的堆内存空间将成为垃圾,所有的垃圾会不定期地被垃圾收集器( Garbage Collector)回收,回收后会被释放掉其所占用的空间虽然Java支持自动的垃圾收集处理,但是在代码的开发过程中应该尽量减少垃圾空间的产生。
*Tips:*关于GC的深入分析
GC在Java中的核心功能就是对内存中的对象进行内存的分配与回收,所以对于GC的理解不能局限于只是进行垃圾收集,还应该知道GC决定了内存的分配。最常见的情况就是当开发者创建一个对象后,GC就会监视这个对象的地址、大小和状态。
对象的引用会保存在栈内存( Stack)中,而对象的具体内容会保存在堆内存
(Heap)中。当GC检测到一个堆中的某个对象不再被栈所引用时,就会不定期的对这个堆内存中保存的对象进行回收。有了GC的帮助,开发者不用再考虑内存回收的情,GC也可以最大限度地帮助开发者防止内存泄露在Java中针对垃圾收集也提供了多种不同的处理分类。
(1)引用计数:一个实例化对象,如果有程序使用了这个引用对象,引用计数加1,当一个对象使用完毕,引用计数减1,当引用计数为0时,则可以回收。
(2)跟踪收集:从 root set(包括当前正在执行的线程、全局或者静态变量JVM Handles、 JNDI Handles)开始扫描有引用的对象,如果某个对象不可到达,则说明这个对象已经死亡(dead),则GC可以对其进行回收。也就是说:如果A对象引用了B对象的内存,那么虚拟机会记住这个引用路径,而如果一个对象没有在路径图中,则就会被回收。
在声明Book类的属性时使用了 private关键字,这样就表示 title与 price两个属性只能够在Book类中被访问,而其他类不能直接进行访问,所以在主类中使用Book类对象直接调用title与price属性时就会在编译时出现语法错误。如果要想让程序可以正常使用,必须想办法让外部的程序可以操作类的属性。所以在开发中,针对属性有这样一种定义:
所有在类中定义的属性都要求使用 private声明,如果属性需要被外部所使用,那么按照要求定义相应的 setter、 getter方法。
下面以 String title为例进行说明。
setter方法主要是设置内容:public void setTitle(String t),有参;
getter方法主要是取得属性内容: public String getTitle(),无参。
构造方法本身是一种特殊的方法,它只在新对象实例构造方法与匿名对象化的时候调用,其定义原则是:方法名称与类名称相同,没有返回值类型声明,同时构造方法也可以进行重载。
注意:构造方法一直存在。
实际上在对象实例化的格式中就存在构造方法的使用,下面通过对象的实例化格式来分析。
①类名称②对象名称=③new④类名称();
①类名称:规定了对象的类型,即对象可以使用哪些属性与方法,都是由类定义的;
②对象名称:如果要想使用对象,需要有一个名字,这是一个唯一的标记;
③new:开辟新的堆内存空间,如果没有此语句,对象无法实例化;
④类名称0:调用了一个和类名称一样的方法,这就是构造方法。
通过以上的简短分析可以发现,所有的构造方法实际上一直在被我们调用。但是我们从来没有去定义一个构造方法,之所以能够使用构造方法,是因为在整个Java类中,为了保证程序可以正常的执行,即使用户没有定义任何构造方法,也会在程序编译之后自动地为类增加一个没有参数、没有方法名称、类名称相同、没有返回值的构造方法。
注意构造方法只有在使用关键字new实例化对象时才会被调用
Tips:构造方法与普通方法的区别:
既然构造方法没有返回值,那么为什么不使用void来声明构造方法呢?普通方法不是也可以完成一些初始化操作吗?
答:构造方法与普通方法的调用时机不同。
首先在一个类中可以定义构造方法与普通方法两种类型的方法,但是这两种方法在调用时有明显的区别:构造方法是在实例化新对象(new)的时候只调用一次;普通方法是在实例化对象产生之后,通过“对象方法”调用多次。
如果在构造方法上使用了void,其定义的结构与普通方法就完全一样,而程序的编译是依靠定义结构来解析的,如果有就不能区分,所以不能有返回值声明。
另外,类中构造方法与普通方法的最大区别在于:构造方法是在使用关键字new的时候直接调用的,是与对象创建一起执行的操作;要通过普通方法进行初始化,就表示要先调用无参构造方法实例化对象,再利用对象调用初始化方法就比较啰唆了。
注意:构造方法的核心作用。
在实际的工作中,构造方法的核心作用是,在类对象实例化时设置属性的初始化内容。构造方法是为属性初始化准备的。在本程序中由于已经明确地定义了一个有参构造方法,就不会再自动生成默认的构造方法,即一个类中至少保留有一个构造方法另外还需提醒的是,此时类中的结构包含属性、构造方法、普通方法,而编写的时候一定要注意顺序:
首先编写属性(必须封装,同时提供 setter、 getter的普通方法)。然后编写构造方法,最后编写普通方法。虽然这些与语法无关,但是每一个程序员都需要养成良好的编码习惯。
在一个类中对构造方法重载时,所有的重载的方法按照参数的个数由多到少,或者是由少到多排列。以下两种排列方式都是规范的。
public Book() public Book(String t, price p)()
public Book(String t){} public Book(String t)
public Book (String t, price p){} public Book()
Tips:关于属性默认值的问题:
在定义一个类时,可以为属性直接设置值,但是这个值只有在构造执行完才会设置,否则不会设置。而构造方法属于整个对象构造过程的最后一步,即是留给用戶处理的步骤在对象实例化的过程中,一定会经历类的加载、内存的分配、构造方法、设置值。
class Book{
private String title = 'Java开发';2.
public Book(){} 1.
}
在本程序中,只有在整个构造都完成后,才会真正将“Java开发”这个字符串的内容设置给 title属性。构造完成之前(在没有构造之前,而不是指的构造方法) title都是其对应数据类型的默认值。
按照之前的内存关系来讲,对象的名字可以解释为在栈内存中保存,而对象的具体内容(属性)在堆内存中保存,这样一来,没有栈内存指向堆内存空间,就是一个匿名对象,如图4所示。
public static void main(String args[]){
new Book("Java",80).getInfo;
}
本程序通过匿名对象调用了类中的方法,但由于匿名对象没有对应的栈内存指向,所以只能使用1次,1次之后就将成为垃圾,并且等待被GC回收释放。
Tips:什么时候使用匿名对象?
在开发中有时定义的对象有名字,而有时使用的是匿名对象,那么到底该怎样区分是使用有名对象还是使用匿名对象?
答:是否使用匿名对象要看用户需求。
可能并不习惯于这种匿名对象的使用,并且会觉得通过匿名对象调用方法的操作有些难以理解,所以没有强制性地一定要使用匿名对象,不习惯可以继续像之前那样声明并实例化对象进行操作,但是对于匿名对象的定义必须清楚,开辟了堆内存空间的实例化对象,只能使用一次,使用一次之后就将被GC回收。
Java Bean是一种Java语言写成的重要组件:
是指符合如下标准的Java类
类是公共的
有一个无参公共构造器
有属性,且有对应get,set方法
即首先开辟内存空间,但数组中内容都是对应数据类型的默认值,若声明是int类型数组,则里面全都是默认值0。
由于数组是种顺序的结构,并且数组的长度都是固定的,所以可以使用循环的方式输出,很明显需要使用for循环,Java为了方便数组的输出,提供了一个“数组名称.length”的属性,可以取得数组长度。
public static void main(String args[]){
int data[] = new int[3];
data[0] = 10;
data[1] = 20;
data[2] = 30;
for (int i=0; i < data.length; i++) {
System.out.println(data[i]+'.');
}
}
本程序首先声明并开辟了一个int型数组data,然后采用下标的方式为数组中的元素进行赋值,由于数组属于有序的结构,所以可以直接使用for循环进行输出。
数组最基础的操作就是声明,而后根据索引进行访间。其最麻烦的问题在于,数组本身也属于引用数据类型,所以范例代码依然需要牵扯到内存分配,与对象保存唯一的区别在于:与对象保存唯一的区别在于:对象中的堆内存保存的是属性,而数组中的堆内存保存的是一组信息。
先声明后开辟数组空间
int data[] = null;
data = new int[3];
即数组定义同时就设置好相应的数据内容
int data[] = new int[]{1,2,3,4,5};
for(int x = 0 ;x <data.length;x++){
System.out.print(data[x]+'、')
}
String stra = "hello";
String strb = new String("hello");
String strc = strb;
System.out.println(stra == strb);
System.out.println(stra == strc);
System.out.println(strb == strc);
false
false
true
因为strb使用关键字new开辟了新的堆内存空间,而在使用“”比较时,比较的只是数值,所以只要地址数值不相同的 String类对象在使用“”比较相等时其结果一定返回的是“ false”;
而strc由于与stb指向了同一块内存空间,所以地址数值相同,那么返回的结果就是“true"。所以“==”在String比较时比较的只是内存地址的教值,并不是内容。
注意:引用类型都可以使用“==”比较。
在整个Java中只要是引用数据类型一定会存在内存地址,而“==”可以用于所有的引用数据类型的比较,但比较的并不会是内容,永远都只是地址的数值内容,这样的操作往往只会出现在判断两个不同名的对象是否指向同一内存空间的操作上。
public boolean equals(String str)
System.out.println(stra.equals(strb));
System.out.println(stra.equals(strc));
System.out.println(strb.equals(strc));
true
true
true
常见面试题:请解释 String类中“=”和“ equals0”比较的区别。
“==”是Java提供的关系运算符,主要的功能是进行数值相等判断,如果用在String对象上表示的是内存地址数值的比较;
equals()”:是由 String提供的一个方法,此方法专门负责进行字符串内容的比较。
在实际的开发中,由于字符串的地址是不好确定的,所以不要使用“==”比较,所有的比较都要通过 equals0方法完成。
任何编程语言都没有提供字符串数据类型的概念,很多编程语言里面都是使用字符数组来描述字符串的定义。同样在Java里而也没有字符串的概念,但由于所有的项目开发中都不可能离开字符串的应用,所以Java创造了属于自己的特殊类String(字符串),同时也规定了所有的字符串要求使用""声明,但是 String依然不属于基本数据类型,所以字符串数据实际上是作为 String类的匿名对象的形式存在。
可以直接利用字符串hello.调用 equals()方法(“hello”. equals(str)),由于equals()方法是 String类定义的,而类中的方法只有实例化对象才可以调用,那么就可以得出一个结论:字符串常量就是 String类的匿名对象。
Tips:实际开发中的字符串比较。
在实际开发过程中,有可能会有这样的需求,由用户自己输入一个字符串,而后判断其是否与指定的内容相同,那么这个时候用户就有可能不输入数据,结果内容就为null。
public static void main(String args[]){
String input = null;
if(input.equals("hello")){
System.out.println("Hello World!");
}
}
此时由于没有输入数据,所以 Input的内容为null,而null对象调用方法的结果将直接导致“NullPointer Exception”,而这样的问题可以通过一些代码的变更来帮助用户回避。
回避 NullPointer Exception问题
String input = null;
if("hello".equals(input)){
System.out.println("Hello World!");
}
本程序直接利用字符串常量来调用 equals()方法,因为字符串常量是一个 String类的匿名对象,所以该对象永远不可能是null,所以将不会出现“ NullPointer Exception”,特别需要提醒,equals()方法内部实际上也存在null的检查,这可以打开Java类的源代码进行查看。
利用直接赋值还可以实现堆内存空间的重用,即采用直接赋值的方式,在出现相同内容的情况下不会开辟新的堆内存空间,而会直接指向已有的堆内存空间。
String stra = "hello";
String strb = "hello";
String strc = "hello";
String strd = "yoo";
System.out.println(stra == strb);
System.out.println(stra == strc);
System.out.println(strb == strc);
System.out.println(stra == strd);
true
true
true
false
Tips:String类采用的设计模式为共享设计模式
在JVM的底层实际上会存在一个对象池(不一定只保存 String对象),当代码中使用了直接赋值的方式定义一个 String类对象时,会将此字符串对象所使用的匿名对象入池保存。如果后续还有其他 String类对象也采用了直接赋值的方式,并且设置了同样的内容时,将不会开辟新的堆内存空间,而是使用已有的对象进行引用的分配,从而继续使用。
如果要明确地调用 String类中的构造方法进行 String类对象的实例化操作,那么一定要使用关键字new,而每当使用关键字new就表示要开辟新的堆内存空间,这块堆内存空间的内容就是传入到构造方法中的字符串数据。
String str = new String("hello");
而之前定义的字符串常量的堆内存空间将不会有任何的栈内存指向,将成为垃圾,等待被GC回收。所以,使用构造方法的方式开辟的字符串对象,实际上会开辟两块空间,其中有一块空间将成为垃圾。
除了内存的浪费外,如果使用构造方法实例化 String类对象,由于关键字new永远表示开辟新的堆内存空间,所以其内容不会保存在对象池中。
如果希望开辟的新内存数据也可以进行对象池的保存,那么可以采用 String类定义的一个手工入池的操作。保存到对象池的语法如下
public String intern():
常见面试题:请解释 String类的两种对象实例化方式的区别。
在使用Sring类进行操作时,还有一个特性是特别重要的,那就是字符串的内容一旦定义不可改变,下面通过一段代码:
String str = "hello";
str = str + "World";
str += "!!!";
System.out.println(str);
本程序首先声明了一个 String类对象,然后修改了两次 String类对象的内容(注意:实际上是发生了两次引用改变)。所以最终 String类对象的内容就是“ Hello World I”。但是在整个操作过程中,只是 String类的对象引用发生了改变,而每个字符串的内容并没有发生改变。下面通过图11进行说明。
通过图可以发现,在进行 String类对象内容修改时,实际上原始的字符串都没有发生变化(最终没有引用的堆内存空间将成为垃圾空间)。而改变的只是 String类对象的引用关系。所以可以得出结论:字符串一旦定义则不可改变。正因为存在这样的特性,所以在开发中应该回避以下代码的编写。
String str = "";
for(int x = 0;x<1000;x++){
str+=x;
}
范例的代码修改了 String对象的引用关系1000次(所有数据类型遇见 String连接操作时都会自动向 String类型转换),并且会产生大量的垃圾空间,所以此类代码在开发中是严格禁止的, String的内容不要做过多频繁的修改。
取出指定索引的字符—使用 charat()方法。
String str = "hello";
char c =str.charAt(0);
System.out.println(c);
h
字符数组与字符串中间的转换
String str = "hello";
char[] data =str.toCharArray();
for (int x = 0; x < data.length; x++) {
System.out.print(data[x]+"、");
h、e、l、l、o、
本程序主要实现了字符串的拆分操作,利用 toCharArray()方法可以将一个字符串拆分为字符数组,而拆分后的字符数组长度就是字符串的长度.
当利用 toCharArray0方法将字符串拆分为字符数组后,实际上就可以针对每一个字符进行操作。下面演示一个字符串小写字母转换为大写字母的操作(利用编码值来处理).
String str = "hello";
char[] data =str.toCharArray();
for (int x = 0; x < data.length; x++) {
data[x]-=32;
}
System.out.print(new String(data));
本程序首先将字符串(为了操作方便,此时的字符串全部由小写字母组成)拆分为字符数组然后使用循环分别处理数组中每一个字符的内容,最后使用 String类的构造方法,将字符数组变为字符串对象。
public static void main(String args[]){
String str = "1234586";
if (isNumber(str)) {
System.out.println("由数字组成");
}else{
System.out.println("非数字组成");
}
}
public static boolean isNumber(String temp){
char[] data = temp.toCharArray();
for (int i = 0; i < data.length; i++) {
if (data[i]>'9'||data[i]<'0') {
return false;
}
}
return true;
}
由数字组成
本程序在主类中定义了一个 isNumber0方法,所以此方法可以在主方法中直接调用。在 isNumber方法中为了实现判断,首先将字符串转换为字符数组,然后采用循环的方式判断每一个字符是否是数字。
(例如:'9’是字符不是数字9),如果有一位不是则返回 false(结束判断),如果全部是数字则返回true.
可以发现在本程序中, isNumber0方法返回的是 boolean数据类型,这是一种真或假的判断,而在Java开发中,针对返回 boolean值的方法习惯性的命名是以isXXX()的形式命名。
String str = "helloworld";
if (str.contains("world")) {
System.out.println("可以查询到数据");
}
直接利用 String类的 contains方法来判断子字符串是否存在, contains方法直接可以直接返回布尔值,这样作为判断条件较为方便。
String str = "helloworld";
String reA = str.replaceAll("l", "_");
String reB = str.replaceFirst("l", "_");
System.out.println(reA);
System.out.println(reB);
本程序利用 replaceAll()与 replaceFirst()两个方法实现了全部以及首个内容的替换,特别需要注意的是,这两个方法都会返回替换完成后的新字符串内容。
String string = "hello www s as a";
String res[] = string.split(" ");
for (int i = 0; i < res.length; i++) {
System.out.println(res[i]+".");
}
hello.
www.
s.
as.
a.
注意:应注意正则表达式的影响,可以进行转义操作。
比如对IP地址的小数点进行分割,此时需要在Java中使用“\”描述。
要想正常执行,就必须对要拆分的“.”进行转义,在Java中转义要使用“\\”(“\\”表示一个“\”)描述。
String string = "192.168.1.1";
String string2[] = string.split("\\.");
for (int i = 0; i < string2.length; i++) {
System.out.print(string2[i]+"、");
}
192、168、1、1、
在实际的开发中,拆分的操作是非常常见的,因为很多时候会传递一组数据到程序中进行处理。例如,现在有如下的一个字符串:“张三:20|李四:21|王五:22|”(姓名:年龄|姓名:年龄|…),当接收到此数据时必须要对数据进行拆分。
String string = "张三:20|李四:30|王五:19";
String string2[] = string.split("\\|");
for (int i = 0; i < string2.length; i++) {
String temp[] = string2[i].split("\\:");
System.out.println(temp[0]+"."+temp[1]);
}
}
张三.20
李四.30
王五.19
Tips:关于lenth的说明:
在学习完 String类中的 length()方法( String对象.length())后,容易与数组中的 length(数组对象.length)属性混淆,在这里, String中取得长度使用的是 length()方法,只要是方法后面都要有“()”,而数组中没有 length()方法只有 length属性
在一个类的定义的方法中可以直接访问类中的属性,但是很多时候有可能会出现方法参数名称与属性名称重复的情况,所以此时就需要利用“this.属性”的形式明确地指明要调用的是类中的属性而不是方法的参数。下面通过代码来验证这一问题。
private String title;
private double price;
public data(String title,double price){
this.title = title;
this.price = price;
}
public String getInfo(){
this.print();
return ...
}
除了可以调用本类方法,在一个类中也可以利用“this”的形式实现一个类中多个构造方法的互相调用。
例如:一个类中存在3个构造方法(无参,有一个参数,有两个参数)。但是不管使用何种构造方法,都要求在实例化对象产生的时候输出一行提示信息:“一个新的Book类对象产生。”(假设这个信息等于50行代码)。使用this即可消除不同构造方法中的重复代码。
消除掉构造方法中的重复代码
class Book {
private String title;
private double price;
public Book(){
System.out.println("一个新的data类对象产生");
}
public Book(String title){
this(); //调用本类无参构造方法
this.title=title;
}
public Book(String title,double price){
this(title); //调用本类有一个参数的构造方法
this.price = price;
}
public String getInfo() {
return this.title+this.price;
}
}
public class data{
public static void main(String args[]) {
Book book = new Book("Java",99);
System.out.println(book.getInfo());
}
}
一个新的data类对象产生
Java99.0
注意:关于this调用构造的限制。
在使用this调用构造方法时,存在两个重要的限制(这些都可以在程序编译时检查出来)
使用 static声明的属性和方法可以由类名称直接调用, static属性是所有对象共享的,所有对象都可以对其进行操作。当一个类中不需要保存属性时,可以考虑将这个类中的方法全部定义为static,这样做可以节约内存空间。