(整理源于网络)
Java SE 2nd day:Object-oriented 01
1、本次课程知识点
● 面向对象的主要特点;
● 类与对象的关系;
● 对象的引用传递的初步分析;
● private封装性的实现;
● 构造方法的定义及使用;
● 匿名对象的操作;
● String类的特点及常用方法;
2、具体内容
2.1 认识面向对象(理解)
面向对象是一种现在最流行的程序设计方法,在面对对象的设计出现之前用的都是面向过程的程序设计方式,至于两者的区别,只能通过一句简单的话概括:面向对象具备很强的重用性,而面向过程的开发,只适合一次性的开发操作。
最早的面向对象的概念是由IBM Smalltalk语言所提出,而后发展出了C++和Java,而对于面向对象本身有三大主要的特点:
● 特点一:封装性,保护内部的东西对外不可见;
● 特点二:继承性,扩充类的功能;
● 特点三:多态性,在某一个范围之内,任意的改变所属的类的形式;
在面向对象之中,除了以上的三个特征之外,还有如下的三个开发过程:
● OOA:面向对象的分析;
● OOD:面向对象的设计;
● OOP:面向对象的程序;
面向对象的唯一好处就在于,与现实生活是完全接轨的,而且没有这么多的意外出现。
2.2 类与对象(重点)
2.2.1 类与对象的基本定义
类和对象是面向对象之中最基本的组成单元,也是以后的所以程序的开发基础,下面首先分析这两者的关系:
● 类:表示某一个群体的共同特征,是一个抽象的概念;
类中主要有两个部分组成:
1、成员(属性):用于描述不同的对象信息;
2、方法:用于完成某些功能的实现;
● 对象: 表示的是一个个体的特征,受到类的控制,类中可以规定出对象的操作行为;
通过以上分析,可以得出以下的结论:
● 在操作的时候应该先有类再有对象;
● 类中规定出来了对象的所有操作的方式(属性、方法),而对象是类的具体使用形式;
● 如果把类比喻成汽车设计图纸的话,那么对象就是那一辆辆的汽车。
类是对象的操作模版,而对象是类的使用实例,对象是其可以看得见摸得着的东西,而类只是一个虚拟的概念。
2.2.2 类和对象的基本操作
类的具体实现,首先在开发之中应该先建立类,而所有的类都使用class关键字定义,类中要包含属性(变量)和方法;
范例:定义一个Person类
class Person{ //定义一个Person类 String name; //属性:表示姓名 int age; //属性:表示年龄 public void tell(){ //表示方法 System.out.println("姓名:"+name+",年龄:"+age); } } |
本类定义了两个属性(name、age)以及一个操作方法 (tell) ,而类定义完成之后并不能立刻使用,必须要有对象,而对象的定义格式如下:
格式一:声明并实例化对象
类名称 对象名称 = new 类名称(); |
格式二:
声明对象: | 类名称 对象名称 = null; |
实例化对象: | 对象 = new 类名称(); |
此时在对象的定义格式上出现了关键字“new”,此处肯定表示要开辟堆内存空间。
当声明并实例化一个类的对象之后,下面就可以通过如下的语法进行类的操作:
● 操作属性:对象.属性
● 操作方法:对象.方法();
范例:定义对象并使用
class Person{ //定义一个Person类 String name; //属性:表示姓名 int age; //属性:表示年龄 public void tell(){ //表示方法 System.out.println("姓名:"+name+",年龄:"+age); } } public class Demo { public static void main(String args[]){ Person per = new Person(); //声明并实例化对象 per.name="张三"; //调用属性 per.age=30; //调用属性 per.tell(); //调用方法 } } |
姓名:张三,年龄:30 |
记住了,区分方法与属性:只要是有“()”的都是属于方法!
如果说现在的Demo类之中,没有设置name和age的属性,那么结果是null和0。
class Person{ //定义一个Person类 String name; //属性:表示姓名 int age; //属性:表示年龄 public void tell(){ //表示方法 System.out.println("姓名:"+name+",年龄:"+age); } } public class Demo { public static void main(String args[]){ Person per = new Person(); //声明并实例化对象 per.tell(); //调用方法 } } |
姓名:null,年龄:0 |
此时,由于没有堆name和age属性设置内容,所以都是默认值,String是一个类默认值就是null,而age是一个int型的数据,默认是0,而且既然是引用数据类型,肯定要有对象的内存关系,实际上这个的内存分析和数组是完成类似;
● 堆内存:保存对象的具体信息,实际上就是属性的信息;
●栈内存:保存了堆内存的地址,简单理解保存的就是对象名称;
以下程序是使用“声明并实例化”的操作来定义对象,实际上对象的操作也可以分为两步来完成:
Person per = null; //声明对象 per = new Person(); //实例化对象 |
此时的内存分析关系图如下:
所以来说,现在规定出对于以后的对象名称,有两种叫法:
● 声明对象:指的是已经开辟了栈内存,但是没有堆内存空间;
● 实例化对象:已经明确的开辟了堆内存空间,则表示对象可以使用。
范例:错误的程序
class Person{ //定义一个Person类 String name; //属性:表示姓名 int age; //属性:表示年龄 public void tell(){ //表示方法 System.out.println("姓名:"+name+",年龄:"+age); } } public class Demo { public static void main(String args[]){ Person per=null; //声明对象 per.age=30; per.name="Alex"; per.tell(); //调用方法 } } |
本程序只是声明了对象,但是并没有进行对象的实例化操作,所以本程序在运行的时候将出现如下的错误信息:
Exception in thread "main" java.lang.NullPointerException at Demo.main(Demo.java:13) |
此时出现了“NullpoiterException”空指向异常,这种异常只发生在引用数据类型之中,表示直接使用了没有开辟堆内存的对象,而且这个问题会一直伴随着程序开发。
2.2.3 深入分析对象的引用传递
以上只是对程序做了一个简单的实现,但是时间的工作之中,对象的引用传递是一个难点,尤其对于初学者而言。
范例:如果说现在产生两个对象,那么这两个对象之间是否会互相影响呢?
class Person{ //定义一个Person类 String name; //属性:表示姓名 int age; //属性:表示年龄 public void tell(){ //操作方法 System.out.println("姓名:"+name+",年龄:"+age); } } public class Demo { public static void main(String args[]){ Person per1=new Person(); //声明对象并实例化对象 Person per2=new Person(); //声明对象并实例化对象 per1.name="张三"; //设置name属性 per1.age=30; //设置age属性 per2.name="李四"; //设置name属性 per2.age=40; //设置age属性 per1.tell(); //调用方法 per2.tell(); //调用方法 } } |
姓名:张三,年龄:30 姓名:李四,年龄:40 |
此时内存关系图如下:
因为使用的是关键字new,所以在任何情况下都表示会开辟新的内存空间。
类本身是属于引用数据类型,那么既然是引用数据类型,则肯定一块堆内存能同时被多个栈内存所指向,所以下面通过一个引用传递的操作来分析:
class Person { // 定义一个Person类 String name; // 属性:表示姓名 int age; // 属性:表示年龄
public void tell() { // 操作方法 System.out.println("姓名:" + name + ",年龄:" + age); } }
public class Demo { public static void main(String args[]) { Person per1 = new Person(); // 声明对象并实例化对象 Person per2 = null; // 声明第二个对象 per1.name = "张三"; // 设置name属性 per1.age = 30; // 设置age属性 per2 = per1; // 将per1的堆地址给了per2 per2.name = "李四"; // 修改name属性 per1.tell(); // 调用方法 } } |
姓名:李四,年龄:30 |
不管是数组也好,还是类也好,只要是引用传递,其基本的操作流程都是一样的,就是不同的栈指向同一个堆,而且记住一点:一个栈内存由于保存的是堆内存的地址,所以只能保存一个地址,即:一个栈只能指向一个堆,但是一个堆内存却可以同时被多个栈内存所指向。
范例:进一步分析如下的题目
class Person{ //定义一个Person类 String name; //属性:表示姓名 int age; //属性:表示年龄 public void tell(){ //操作方法 System.out.println("姓名:"+name+",年龄:"+age); } } public class Demo { public static void main(String args[]){ Person per1=new Person(); //声明对象并实例化对象 Person per2=new Person(); //声明第二个对象 per1.name="张三"; //设置name属性 per1.age=30; //设置age属性 per2.name="李四"; per2.age=20; per2=per1; //将per1的堆地址给了per2 per2.name="王五"; //修改name属性 per1.tell(); per2.tell(); //调用方法 } } |
姓名:王五,年龄:30 姓名:王五,年龄:30 |
通过程序的分析可以发现,在java之中,不使用的堆内存空间(没有栈内存指向)就将成为垃圾,所有的垃圾在默认情况下将等待GC(garbagecollection垃圾收集器)进行不定期的回收,而在实际的开发之中,为了代码的性能,所以垃圾的产生应该越少越好。
2.3 封装性(重点)
所谓封装性指的就是类内部的操作对外部不可见,例如,现在有如下一道程序:
class Person { // 定义一个Person类 String name; // 属性:表示姓名 int age; // 属性:表示年龄
public void tell() { // 操作方法 System.out.println("姓名:" + name + ",年龄:" + age); } }
public class Demo { public static void main(String args[]) { Person per1 = new Person(); per1.name = "张三"; // 设置name属性 per1.age = -30; // 设置age属性 per1.tell(); } } |
姓名:张三,年龄:-30 |
此时将Person对象的age属性设置为了“-30”岁,但并没有造成程序错误,因为int型的数据本身就包含了负数,但是没有任何人的年龄是负数,所以说现在是业务上出现了问题。
而造成此问题的关键就在于Person类中的age属性可以直接被外部所访问,所以这种情况下要想解决此类问题,就必须想办法将age属性变为外部不可见,而此种操作可以通过封装性实现,但是由于java之中封装性的概念比较多,所以今天只是讲解一种最简单的封装使用,直接用private关键字定义。
private String name; // 属性:表示姓名 private int age; // 属性:表示年龄 |
现在在name和age属性上使用了private关键字。则在编译时出现如下错误提示:
一旦使用了private声明之后,这两个属性就表示只能在Person类的内部进行访问,但是如果现在在外部想访问这些属性,则按照java的开发标准来讲,就必须定义getter、setter
● setter:用于设置属性的内容,可以增加一些检查的措施;
● getter:用于取得属性的内容,只是简单的返回;
范例:编写setter、getter
class Person { // 定义一个Person类 private String name; // 属性:表示姓名 private int age; // 属性:表示年龄
public void setName(String n) { // 注意setName的书写,N要大写。 name = n; }
public void setAge(int a) { if (a >= 0 && a <= 250) { age = a; } }
public String getName() { return name; }
public int getAge() { return age; }
public void tell() { // 操作方法 System.out.println("姓名:" + name + ",年龄:" + age); } }
public class Demo { public static void main(String args[]) { Person per1 = new Person(); per1.setName("张三"); per1.setAge(-30); per1.tell(); } } |
姓名:张三,年龄:0 |
注意:此次并没有用到getName和getAge这两个方法
在以后的开发之中,类中的所以属性都必须使用private关键字封装,所以被封装的属性必须使用setter、getter方法设置和取得,而且setter、getter的编写必须符合规范。
本处只是对封装性做了一个最简单的实现,而实际上封装的完整概念将再最后为大家进行总结,封装实际上就是对于访问权限的控制操作。
2.4 构造方法(重点)
在之前给出过一个对象的实例化格式:
类名称 对象名称 = new 类名称(); |
可以发现在实例化对象的时候需要执行一个“类名称()”的代码,在之前提示过,只有在方法的调用上才会出现“()”,所以此处所调用的方法实际上就是一个构造方法。
对于java程序而言,如果用户没有在类中明确定义一个构造方法的话,会自动生成一个无参的,什么都不做的构造方法,所以以上的对象实例化的时候就是由java自动为用户生成的构造方法,而如果用户想要明确的定义一个自己的构造方法,则必须满足如下几个要求:
● 构造方法的方法名称与类名称相同;(用于区别普通方法)
● 构造方法定义的时候没有返回值声明;
范例:定义构造方法,定义无参构造(默认生成的也是此类构造)
class Person { // 定义一个Person类 private String name; // 属性:表示姓名 private int age; // 属性:表示年龄
public Person(){ //无参构造方法 System.out.println("***********"); } }
public class Demo { public static void main(String args[]) { Person per =null; //声明对象 } } | class Person { // 定义一个Person类 private String name; // 属性:表示姓名 private int age; // 属性:表示年龄
public Person(){//无参构造方法 System.out.println("***********"); } }
public class Demo { public static void main(String args[]) { Person per =null; //声明对象 per=new Person(); //实例化对象 } } |
(无输出) | *********** |
通过代码的演示可以发现,构造方法是在一个类的对象实例化的时候才进行调用的,所以构造方法在开发之中的主要目的是为类中的属性初始化,这个初始化指的是可以由用户定义的默认值,也可以由外部输入。
范例:通过构造方法传递属性内容
class Person { // 定义一个Person类 private String name; // 属性:表示姓名 private int age; // 属性:表示年龄
public Person(String name, int age) { setName(name); setAge(age); }
public void setName(String n) { name = n; }
public void setAge(int a) { if (a >= 0 && a <= 250) { age = a; } }
public String getName() { return name; }
public int getAge() { return age; }
public void tell() { // 操作方法 System.out.println("姓名:" + name + ",年龄:" + age); } }
public class Demo { public static void main(String args[]) { Person per = null; // 声明对象 per = new Person("张三", 30); // 实例化对象 per.tell(); } } |
姓名:张三,年龄:30 |
所以现在有以下两个注意点:
● 第一点:一个类中至少有一个构造方法;
● 第二点:如果一个类中定义属性的时候给出了默认值的话,则这个默认值,必须在构造方法执行完毕之后才可以被赋予;
private String name="张三"; // 属性:表示姓名,此处定义了默认值 private int age; // 属性:表示年龄
public Person() { // 无参构造方法 } |
构造方法本身既然是方法,那么就可以按照之前方法重载的概念在一个类中定义多个构造方法。
class Person { // 定义一个Person类 private String name; // 属性:表示姓名 private int age; // 属性:表示年龄
public Person() { System.out.println("无参构造方法。"); }
public Person(int age) { setAge(age); // }
public Person(String name) { this.setName(name); // 如果此时需要明确的表示出这个方法。则加this,此处不用加也行 }
public Person(String name, int age) { setName(name); setAge(age); }
public void setName(String n) { name = n; }
public void setAge(int a) { if (a >= 0 && a <= 250) { age = a; } }
public void tell() { // 操作方法 System.out.println("姓名:" + name + ",年龄:" + age); } }
public class Demo { public static void main(String args[]) { Person per = null; // 声明对象 per = new Person(); // 实例化对象 per.tell(); } } |
无参构造方法。 姓名:null,年龄:0 |
如果以后一个类之中存在多个构造方法的话,建议的编写顺序:参数少的写在前面,参数多的写在后面,或者是与之相反,总之要有编写的顺序。
问题:为什么构造方法的定义上不写返回值类型,如果不返回写一个void不也可吗?
如果构造方法上写上了void的话,则表示就是一个普通方法了,普通方法是在实例化之后调用的,而构造方法是在对象实例化的时候同时调用的。
2.5 匿名对象(重点)
匿名 = 没有名字,按照之前的分析,所有的对象信息都保存在栈和堆,其中栈保持的是地址(名字),而堆保存的是对象的具体信息(是真正有用的东西),所以所谓的匿名对象就是指只开辟了堆内存的对象。
public class Demo { public static void main(String args[]) { new Person("张三",50).tell(); } } |
姓名:张三,年龄:50 |
由于匿名对象没有任何的名字,所以在使用之后将成为垃圾,等待GC进行回收,所以以后只使用一次的实例化对象就使用匿名对象表示。
2.6 思考题(重点)
现在要求定义一个表示雇员的操作类(emp),里面有如下的属性:雇员编号、姓名、职位、基本工资、奖金,要求可以取得一个雇员的完整信息,也可以计算出雇员的月薪和年薪收入。
本程序的最终效果与之前所写的类是一样的,以后面对的类都有一个统一的名称——简单java类,即:只包含属性、setter、getter方法的操作类;
对于所有的简单java类开发要求如下:
● 根据已有的要求定义类的名称——Emp.java;
● 根据给出的提示编写相应的属性,但是所有的属性都必须使用private关键字进行封装;
● 封装之后的属性如果需要被外部所操作,则要编写对应的setter、getter方法;
● 类中的所有方法都不允许直接使用System.out.println()输出,所有的输出内容返回给被调用处输出;
● 可以编写构造方法进行属性内容的传递,但是类中一定要保留一个无参构造方法。
范例:Emp定义:
class Emp{ //定义Emp类 private int empno; private String ename; private String job; private double sal; private double comm;
public Emp(){} public Emp(int empno,String ename,String job,double sal,double comm){ this.setCom(comm); this.setEmpno(empno); this.setEname(ename); this.setJob(job); this.setSal(sal); } public String getEmpInfo(){ return "雇员信息: "+"\n"+ "\t|- 编号: "+this.getEmpno()+"\n"+ "\t|- 姓名: "+this.getEname()+"\n"+ "\t|- 职位: "+this.getJob()+"\n"+ "\t|- 工资: "+this.getSal()+"\n"+ "\t|- 奖金: "+this.getComm()+"\n"+ "\t|- 月薪: "+this.salary()+"\n"+ "\t|- 年薪: "+this.income(); }
public double salary(){ return sal+comm; } public double income(){ return this.salary()*12; }
public void setEname(String n){ ename=n; } public void setEmpno(int n){ empno=n; } public void setJob(String j){ job=j; } public void setSal(double s){ sal=s; } public void setCom(double c){ comm=c; }
public int getEmpno(){ return empno; } public String getEname(){ return ename; } public String getJob(){ return job; } public double getSal(){ return sal; } public double getComm(){ return comm; }
} |
范例:编写测试程序,测试Emp类
public class Demo { public static void main(String args[]) { Emp emp=new Emp(7369,"SMISH","CHERK",800,10); System.out.println(emp.getEmpInfo()); } } |
雇员信息: |- 编号: 7369 |- 姓名: SMISH |- 职位: CHERK |- 工资: 800.0 |- 奖金: 10.0 |- 月薪: 810.0 |- 年薪: 9720.0 |
2.7 String类(核心重点)
在java中,String是一个较为特殊的类,下面通过若干个分析进行一个初期的全面介绍。
2.7.1 String类对象的实例化方式
String肯定是一个类,但是这个类的对象却有两种实例化形式,分别如下:
● 直接将一个使用“"”定义的字符串赋给String对象;
● 使用String类中的构造方法:public String(String str)
范例:直接赋值
public static void main(String args[]){ String str="Hello World."; System.out.println(str); } |
Hello World. |
范例:通过构造方法完成
public static void main(String args[]){ String str=new String("Hello World."); System.out.println(str); } |
Hello World. |
此时只需暂时知道有两种方式可以实例化String类的对象,而这种方式的区别之后再解释。
2.7.2 String类对象的比较
在java之中会直接提供一个“= =”来的操作符来进行两个变量的比较。
范例:使用“= =”进行比较
public static void main(String args[]){ int x=10; int y=10; System.out.println(x==y); } |
true |
以上是连个数字相比较所以结果是true,下面使用字符串比较,
范例:进行两个字符串的比较,使用“= =”完成
public static void main(String args[]){ String str1="Hello"; String str2=new String("Hello"); String str3=str2; System.out.println(str1==str2); System.out.println(str1==str3); System.out.println(str2==str3); } } |
false false true |
字符串的内容是一样的,但比较的结果却不都是一样。
通过内存关系可以发现使用能够“= =”实际上就是进行了数值比较,而用在对象上完成的是两个对象的内存地址的数值比较,并没有比较字符串的内容。
如果要想进行字符串内容的比较,则需要使用String类的一个比较方法完成;
●内容比较:public.boolean equals(String other);
public static void main(String args[]){ String str1="Hello"; String str2=new String("Hello"); String str3=str2; System.out.println(str1.equals(str2)); System.out.println(str1.equals(str3)); System.out.println(str2.equals(str3)); } |
true true true |
此时返回的结果全部都是true,证明现在比较的的确是字符串的内容,即:严格来说是比较了堆内存中的内容。
面试题:请解释String的两种比较方式?
● 在String之中可有使用“= =”和equals()两种操作来进行字符串的比较;
● “= =”比较的是两个字符串的内存地址数值,属于数值比较;
● equals()是String类中提供的一个方法,可以用于字符串内容的比较;
最简单的做法就是比较字符串的时候永远使用equals()就可以了。
2.7.3 一个字符串常量就是String的匿名对象
很明显,只要使用了“"”的内容就是字符串,但是在java之中,字符串并不属于一个基本数据类型,所以java会自动的把一个字符串常量当作一个String的匿名对象来处理。
范例:验证字符串是匿名对象
public static void main(String args[]){ String str="Hello"; System.out.println("Hello".equals(str)); } |
true |
"Hello".equals(str)中的“Hello”实际就是一个匿名对象,因为它调用了equals()对象方法。
小技巧:在开发之中采用以上方式可以避免NullPointerException
如果现在要判断一个字符串对象是否与一个字符串常量相等,可以有以下两种写法:
写法一:
正常情况: | 存在隐患: |
public static void main(String args[]){ String str="Hello"; System.out.println(str.equals("Hello")); } | public static void main(String args[]){ String str=null; System.out.println(str.equals("Hello")); } |
True | NullPointerException |
使用写法一虽然在正常情况下可以实现比较操作,但是这种做法有可能会造成空间指向异常。
写法二:
public static void main(String args[]) { String str = null; System.out.println("Hello".equals(str)); } |
false |
通过字符串常量equals()方法,那么这个常量永远不会是null,所以可以避免指向空间异常,那么在以后的开发之中,建议按照此种方式进行比较。
2.7.4 两种实例化方式的区别
对于String的对象而言,现在给出了两种实例化方式,那么在开发之中使用能够哪种方式更好呢?为了解决这个问题,下面通过一些代码及内存的关系图来加以说明。
1、使用直接赋值
public static void main(String args[]){ String str="Hello"; System.out.println("Hello".equals(str)); } |
true |
本程序只开辟了一块堆内存和一块栈内存,除了以上的特点之外,下面再来观察如下程序:
public static void main(String args[]){ String str1="Hello"; String str2="Hello"; String str3="Hello"; System.out.println(str1==str2); System.out.println(str1==str3); System.out.println(str2==str3); } |
true true true |
在java中存在一种称为共享设计模式的概念,所谓的共享设计指的是在JVM,之中为用户提供一个对象池,当用户创建了一个新的且池中没有新的对象时,除了这个对象分配内心之外,还会在对象池之中进行保留,以后如果有其他对象声明了与之一样的内容时,不会再重复声明,而是从对象池中取出内容继续使用,而String就正好利用了此机制。
当用户采用直接赋值实例化String对象的时候,如果是第一次定义,则会自动的将对象的内容(字符串内容)保存在字符串对象池之中,以后如果其他的字符串对象依然采用直接赋值的话,一块直接通过对象池取出已经保存的内容继续使用,而不会再重新开辟新的空间,所以此时的内存关系图如下:
2、通过构造方法完成
在String类提供了一个构造方法用于String
String str=new String("Hello"); |
本操作会开辟两块堆内存空间,其中有一块空间将称为垃圾,而且使用这种方法定义的字符串对象内容,本身不能自动入池;
public static void main(String args[]){ String str1=new String("Hello"); String str2="Hello"; String str3="Hello"; System.out.println(str1==str2); System.out.println(str1==str3); System.out.println(str2==str3); } |
false false true |
但是用户此时却可以通过String类中提供的一个intern()方法手工入池:public.String intern();注:英,intern有实习生的意思。
public static void main(String args[]){ String str1=new String("Hello").intern(); String str2="Hello"; String str3="Hello"; System.out.println(str1==str2); System.out.println(str1==str3); System.out.println(str2==str3); } |
true true true |
虽然可以手工入池,但是很明显,这种操作并不便于用户的开发。
面试题:请解释String对象的两种实例化方法的区别?
● String对象的实例化方式有两种:一种是直接赋值,另外一种是通过构造方法完成;
● 直接赋值:只开辟了一个堆内存空间,而且采用了共享设计模式,可以自动的入池,以备下次对象继续使用;
● 构造方法:会开辟两块内存空间,其中有一块空间将成为垃圾,而且不会自动入池,但是可以使用intern()方法进行手工入池;
● 从开发角度而言,很明显使用直接赋值的方式会更好一些。
2.7.5 字符串的内容一旦声明则不可改变
对于字符串的内容在java之中规定是不能改变的,即:如果定义了字符串,则肯定无法修改,但有如下问题:
public static void main(String args[]){ String str=new String("Hello"); str+=" World"; str=str+"!!!!"; System.out.println(str); } |
Hello World!!!! |
明显可以修改!此时,程序的输出结果是“Hello World!!!!”,明明是改变了,为什么非说不能改变呢?
下面同内存关系图分析一下;
笔记:以上也就说明了在Java中只要是遇到“ "…"”都会开辟一个堆内存。
通过以上的分析可以发现,实际上对于String中的字符串内容并没有任何变化,而最后的改变实际上改变的是String对象的内存地址的指向,所以字符串内容依然没有变化,但这样绝对含有垃圾产生,所以在开发之中对于以下的代码必须回避:
public static void main(String args[]){ String str=""; for(int i=0;i<1000;i++){ str+=i; } System.out.println(str); } |
此时的代码实际上相当于“断开--链接”1000次,而且会产生大量的垃圾空间,所以这种代码的性能是很差的,但是在开发之中对于此类代码也并非不能实现,只是不能通过String实现,而通过StringBuffer实现。
2.8 字符串的操作方法(核心重点,背)
在开发之中,String类使用的几率是最高的,基本上只要是程序都有String,在String类中提供了许多的操作方法,这些方法下面进行一些归类的讲解,对于以下的所讲解的全部方法,要求记住方法的作用,方法的名称,参数的类型及个数,返回值的类型。
所有的操作方法可以直接通过查找JDK Doc文档取得。
2.8.1 字符与字符串
在许多的编程语言之中,都会把字符当作字符串来处理,这一点在java中也是有所体现的,这些方法可以直接通过文档查询:
No | 方法名称 | 类型 | 描述 |
1 | public String(char[] value) | 构造 | 将字符数组变为String类 |
2 | public String(char[] value,int offset,int count) | 构造 | 将部分字符数组变为String |
3 | public char charAt(int index) | 普通 | 返回指定位置上的字符 |
4 | public char[] toCharArray() | 普通 | 将字符串变为字符数组 |
范例:通过程序来完成字符串和字符数组的互相转换
public static void main(String args[]){ String str="hello world"; char c[]=str.toCharArray(); //将字符串变为字符数组 for(int i=0;i<str.length();i++){ System.out.print(c[i]); c[i]-=32; //转大写 } System.out.println("\n"+new String(c)); //全部字符数组变为字符串 System.out.println(new String(c,1,4)); //部分字符数组变为字符串 } |
hello world HELLO WORLD ELLO |
现在也可以使用charAt()方法取得一个字符串的指定位置的字符;
思考题:判断一个字符串是否由数字组成
● 首先将字符串变为字符数组;
● 之后字符数组中的每一个元素进行判断,判断其是否是数字,数组范围:‘0’~‘9’。
public static void main(String args[]) { String str = "0123456789"; System.out.println(isNumber(str) ? "纯数字组成" : "非纯数字组成"); }
public static boolean isNumber(String data) { char c[] = data.toCharArray(); // 变为字符数组 for (int i = 0; i < c.length; i++) { if (c[i] > '9' || c[i] < '0') { // 不是数字 return false; } } return true; } |
纯数字组成 |
对于方法的命名,除了之前给的setter、getter之外,如果方法的返回值是boolean型数据的话,方法名称最好以“is”开头,例如:isNumber();
2.8.2 字节数组与字符串
字节数组也可以和字符串进行转换,
No | 方法名称 | 类型 | 描述 |
1 | public String(byte[] bytes) | 构造 | 将全部字节数组变为字符串 |
2 | public String(byte[] bytes,int offset,int length) | 构造 | 将部分字节数组变为字符串 |
3 | public byte[] getBytes() | 普通 | 将字符串变为字节数组 |
4 | public byte[] getBytes(String charsetName) throws UnsupportedEncodingException | 普通 | 进行转码操作 |
范例:验证字符串和字节数组
public static void main(String args[]) { String str = "helloworld"; byte data[] = str.getBytes(); // 字符串变为字符数组 for (int i = 0; i < data.length; i++) { data[i] -= 32; System.out.print(data[i] + "\t"); } str = new String(data); // 将字节数组data转为字符串并存储在str中 System.out.println("\n" + str); System.out.println(new String(data, 0, 5)); // 将data中的从下标为0的5字节数据转变为字符串,但没有存储在变量中,用完将成为垃圾 } |
72 69 76 76 79 87 79 82 76 68 HELLOWORLD HELLO |
通过以上的代码可以发现这种操作和之前的字符数组很相似,但是字节的操作主要用于二进制数据的传输上,在java IO操作中会继续使用
2.8.3 字符串的比较
equals()方法可以用于字符串内容的比较,对于比较的操作有如下几个方法:
No | 方法名称 | 类型 | 描述 |
1 | public boolean equals(Object anObject) | 普通 | 进行字符串内容的比较,区分大小写 |
2 | public boolean equalsIgnoreCase(String anotherString) | 普通 | 进行字符串内容的比较,不区分大小写 |
3 | public int compareTo(String anotherString) | 普通 | 判断字符串的大于、小于、等于 |
4 | public int compareToIgnoreCase(String anotherString) | 普通 | 判断时,忽略大小写 |
范例:验证区分大小写的比较
public static void main(String args[]){ String str1="hello"; String str2="Hello"; System.out.println(str1.equals(str2)); System.out.println(str1.equalsIgnoreCase(str2)?"相等":"不相等"); } |
false 相等 |
对于String类中的compareTo()方法使用需要注意,此方法返回的是int型数据,这个数据有三种形式:
● 大于:返回大于0的数字;
● 小于:返回小于0的数字;
● 等于:返回等于0的数字;
范例:比较字符串的大小
public static void main(String args[]){ String str1="hello"; String str2="Hello"; System.out.println(str1.compareTo(str2)); System.out.println("Hello".compareTo("hello")); System.out.println(str1.compareToIgnoreCase(str2));
} |
32 -32 0 |
但是对于compareTo()方法而言,除了返回以上的几种数据之外,课也可返回另外三种:大于(1),小于(-1),等于(0)。
2.8.4 字符串检索
可以在String中判断是否存在某些字符串,而可以实现此操作的方法有如下几个:
No | 方法名称 | 类型 | 描述 |
1 | public boolean contains(CharSequence s) | 普通 | 判断指定的字符串是否存在 |
2 | public int indexOf(int ch) | 普通 | 从头查找指定的字符串是否存在,存在则返回字符串的索引,不存在则返回-1 |
3 | public int indexOf(int ch,int fromIndex) | 普通 | 从指定位置开始检索,如果没找到返回-1 |
4 | public int lastIndexOf(int ch) | 普通 | 从后向前查找指定字符串的位置 |
5 | public int lastIndexOf(int ch, int fromIndex) | 普通 | 从指定位置开始由后向前查找 |
6 | public boolean startsWith(String prefix) | 普通 | 判断是否以指定的字符串开头 |
7 | public boolean endsWith(String suffix) | 普通 | 判断是否以指定的字符串结尾 |
范例:验证以上方法
public class StringDemo { public static void main(String args[]){ String str="##Hello World**"; System.out.println(str.contains("hello")); System.out.println(str.indexOf("World")); System.out.println(str.indexOf("World",9)); System.out.println(str.startsWith("##")); System.out.println(str.endsWith("**")); int ind=0; //先将str.indexOf()方法的返回值给ind变量,随后再判断ind的内容是否是-1 if((ind=str.indexOf("World"))!=-1){ System.out.println("the result of index:"+ind); } } |
false 8 -1 true true the result of index:8 |
说明:关于字符串查找操作
通过上面的演示可以发现使用contains()和indexOf()方法都可以完成字符串的查找操作,但是从实际开发来讲,都是使用contains()方法较多,所以建议使用contains()。
2.8.5 字符串替换
在oracle中有一个函数可以完成字符串替换,是replace()函数,同样的功能在String类也有,方法如下:
No | 方法名称 | 类型 | 描述 |
1 | 普通 | 将满足条件的内容全部替换 | |
2 | public String replaceFirst(String regex, String replacement) | 普通 | 替换第一个满足条件的内容 |
范例:演示替换操作
public static void main(String args[]){ String str="Hello World"; System.out.println(str.replaceAll("l","?")); System.out.println(str.replaceFirst("l","?")); } |
He??o Wor?d He?lo World |
对于替换操作,以上只是列出了字符串的直接替换,而在String类中也有许多替换操作,但是这些操作一般使用较少,所以不一一列出。
2.8.6 字符串截取
在oracle之中使用substr()函数完成字符串的截取操作,而同样的功能在String类中也有。
No | 方法名称 | 类型 | 描述 |
1 | public String substring(int beginIndex) | 普通 | 从头截取到结尾 |
2 | public String substring(int beginIndex, int endIndex) | 普通 | 截取中间的部分内容 |
范例:字符串截取
public static void main(String args[]){ String str="Hello World"; System.out.println(str.substring(6)); System.out.println(str.substring(2,4)); } |
World ll |
在String类之中的字符串截取操作和oracle()中是不一样的,String类中下标只能从0开始。
2.8.7 字符串拆分
在String类中提供了一个可以将字符串按照指定内容拆分的操作
No | 方法名称 | 类型 | 描述 |
1 | 普通 | 全拆分 | |
2 | 普通 | 拆分成指定的个数 |
范例:验证拆分功能实现
范例:更细节的拆分
public class Hello { public static void main(String args[]) { String str = "a1223 a34 a56 a78 a90 "; //结尾有3个空格 String temp[] = str.split(" "); // 全拆分,忽略结尾空格 System.out.println("-----全拆分,长度:" + temp.length + "-----"); for (int i = 0; i < temp.length; i++) { System.out.println(temp[i]); } System.out.println("-----结束-----" + "\n"); String data[] = str.split("2", 3); // 正数,拆分2 System.out.println("-----拆分限度为3,长度:" + data.length + "-----"); for (int i = 0; i < data.length; i++) { System.out.println(data[i]); } System.out.println("-----结束-----" + "\n"); data = str.split(" ", 7); // 将结尾的空格也进行拆分 System.out.println("-----拆分限度为7,长度:" + data.length + "-----"); for (int i = 0; i < data.length; i++) { System.out.println(data[i]); } System.out.println("-----结束-----" + "\n"); data = str.split(" ", 0); //limit为0时,忽略结尾的空格 System.out.println("-----拆分限度为0,长度:" + data.length + "-----"); for (int i = 0; i < data.length; i++) { System.out.println(data[i]); } System.out.println("-----结束-----" + "\n"); data = str.split(" ", -2); //limit为负数时,拆分包括结尾空格,负数的数值对结果没有影响 System.out.println("-----拆分限度为-2,长度:" + data.length + "-----"); for (int i = 0; i < data.length; i++) { System.out.println(data[i]); } System.out.println("-----结束-----" + "\n"); } } |
-----全拆分,长度:5----- a1223 a34 a56 a78 a90 -----结束-----
-----拆分限度为3,长度:3----- a1
3 a34 a56 a78 a90 -----结束-----
-----拆分限度为7,长度:7----- a1223 a34 a56 a78 a90
-----结束-----
-----拆分限度为0,长度:5----- a1223 a34 a56 a78 a90 -----结束-----
-----拆分限度为-2,长度:8----- a1223 a34 a56 a78 a90
-----结束-----
|
由上面的范例可以看出,在split(str,n)中的n可以为正数、负数、0。
面试题:现在给定一个IP地址,要求将其拆分
● 很明显现在的IP地址应该按照“.”进行拆分;
public static void main(String args[]){ String str="192.168.0.100"; String temp[]=str.split("\\."); //此处如果不加“\\”,则无结果输出,因为java中“.”表示任意字符 for(int i=0;i<temp.length;i++){ System.out.println(temp[i]); } } |
192 168 0 100 |
在开发之中,split()方法和replaceAll()方法比较特殊,因为都需要“regex”支持,此为正则表达式,如果以后拆不开的问题,就使用“\\”(表示一个“\”)进行转义。
对于需要转义的情况,只观察方法定义的参数是否存在“regex”单词。
2.8.8 其他方法
除了以上的可以归纳的方法之外,在String中也有一些小的功能的方法
No | 方法名称 | 类型 | 描述 |
1 | public boolean isEmpty() | 普通 | 判断是否是空字符,不是null |
2 | public int length() | 普通 | 取得字符串内容的长度 |
3 | public String toLowerCase() | 普通 | 所有内容变为小写 |
4 | public String toUpperCase() | 普通 | 所有内容变为大写 |
5 | public String trim() | 普通 | 去掉左右空格,中间的无法去掉 |
范例:验证以上方法
public static void main(String args[]){ String str=(" Hello World "); System.out.println(str.length()+"\t"+str.trim().length()); System.out.println(str.trim()); //trim()方法只是去掉左右空格,中间的不处理 System.out.println("".isEmpty()+" "+str.isEmpty()); System.out.println("to upper case:"+str.toUpperCase()); System.out.println("to lower case:"+str.toLowerCase()); } |
24 14 Hello World true false to upper case: HELLO WORLD to lower case: hello world |
但是需要强调的是length()方法,因为在数组中有一个length属性可以求出数组的长度,但是这个属性操作的时候后面是没有“( )”的,可是String类中的length()是一个方法,所以要有“( )”。
在oracle中有一个initcap()函数,这个函数可以让开头首字母大写,但是在String类中并没有提供此方法,所以用户可以自己实现一个:
public static void main(String args[]){ String str=("hello"); System.out.println(initcap(str)); } public static String initcap(String str){ return str.substring(0, 1).toUpperCase()+str.substring(1); } |
Hello |
虽然这样的方法在JDK中没有直接提供,但是一些其他组建包中是有所提供的,例如:commons组建包。
3、总结
1、面向对象的三个特征:封装、继承、多态
2、类和对象的关系、定义、引用传递
3、构造方法的使用
4、private封装的实现
5、简单java类的开发原则:有时候可以成为POJO,也可以成为VO(Value Object)。
● 简单java类的定义原则:只有属性及setter、getter方法所组成的类;
● 根据要求定义出类的名称,并编写属性及对应的setter、getter方法,而且所有的属性必须使用private封装;
● 类中可以提供构造方法为属性进行初始化,但是一定要保留有一个无参构造;
● 类中的所有内容都不允许直接输出,必须返回给被调用初输出;
6、String类的特点及相关的方法
● String类的特点:
|- 有两种实例化方式;
1. 直接字符串赋值:在堆内存之中只开辟一块空间,可以自动入池,以备下次继续使用;
2. 通过构造方法完成:会产生两块堆内存空间,不会自动入池,可以使用intern()方法手工入池;
|- 字符串比较:
1. “==”:比较的是两个字符串所在的堆内存地址的比较,属于数值比较;
2. “equals”:是String类中提供的一个方法,进行内容的比较;
|- 一个字符串常量就是String的匿名对象,是会占堆内存的;
|- 字符串的内容一旦声明之后则不可改变,改变的只是内存地址的指向;
● String的常用方法:
|- 字符数组和字符串转换:
Ø 【构造】public String(char c[ ]);
Ø 【构造】public String(char c[ ],intoffset,int len);
Ø 【普通】public char[] toCharArray();
Ø 【普通】public char charAt();
|- 字节数组和字符串转换:
Ø 【构造】public String(byte b[ ]);
Ø 【构造】public String(byte b,intoffset,int len);
Ø 【普通】public byte[ ] getBytes();
Ø 【构造】public byte[ ] getBytes(Stringcharset);
|- 字符串拆分:
Ø 【普通】public String[ ] split(Stringregex);
Ø 【普通】public String[ ] split(Strngregex,int size);
|- 字符串比较:
Ø 【普通】public boolean equals(Stringother);
Ø 【普通】public booleaneuqalsIgnoreCase(String other);
Ø 【普通】public int compareTo(Stingstr); → >0、<0、=0
|- 字符串检索:
Ø 【普通】public boolean contains(Stringstr);
Ø 【普通】public int indexOf(String str);
Ø 【普通】public int indexOf(Stringstr,int offset);
Ø 【普通】public int lastIndexOf(Stringstr,);
Ø 【普通】public int lastIndexOf(Stringstr,int offset);
Ø 【普通】public boolean startsWith(Stringstr);
Ø 【普通】public boolean endsWith(Stringstr);
|- 字符串截取:
Ø 【普通】public String substring(intbegin);
Ø 【普通】public String substring(int begin,intend);
|- 字符串替换:
Ø 【普通】public Sting replaceAll(StingoldStr,Sting newStr);
Ø 【普通】public StringreplaceFirst(String oleStr,String newStr)
|- 其他方法
Ø 【普通】public int length();
Ø 【普通】public String trim();
Ø 【普通】public boolean isEmpty();
Ø 【普通】public String toLowerCase(Stringstr);
Ø 【普通】public String toUpperCase(Stringstr);
4、作业
1、现在给出如下一个字符串格式:“姓名:成绩 | 姓名:成绩”,例如:给定的字符串是:“Tom:90 | Jerry:80 | Tony:89”,要求可以对数据进行处理,将数据按照如下的形式显示:
● 例如:显示格式 姓名:Tom,成绩:90;
DIY: |
import java.util.Scanner;
public class DisplayInfo{ public static void main(String args[]) { Scanner scan = new Scanner(System.in); String str; str = scan.nextLine(); String strTemp[] = str.split("\\|"); for (int i = 0; i < strTemp.length; i++) { int indexNum = strTemp[i].indexOf(":"); System.out.println("Name:" + strTemp[i].substring(0, indexNum) + ", Grade" + strTemp[i].substring(indexNum)); } } } |
Tom:90|Jerry:87|Marry:82|Jessica:79 Name:Tom, Grade:90 Name:Jerry, Grade:87 Name:Marry, Grade:82 Name:Jessica, Grade:79 |
Answers: |
public static void main(String args[]) { String str="Tom:90|Jerry:87|Marry:82|Jessica:79"; String temp[]=str.split("\\|"); for(int i=0;i<temp.length;i++){ String result[]=temp[i].split(":"); System.out.println("Name: "+result[0]+", Grade:"+result[1]); } } |
Name: Tom, Grade:90 Name: Jerry, Grade:87 Name: Marry, Grade:82 Name: Jessica, Grade:79 |
此题主的目的是训练字符串的拆分操作,记住,拆不开的问题肯定会有,所以只要拆不开的内容就转义“\\”。
2、给定一个email地址,要求验证其是否这确,提示:可以简单的验证一下,重点验证“@”和“ . ”。
下面是几个验证标准:
● 最短email长度是5:a@a.a
● @和. 不能作为开头和结尾;
● @和. 顺序要有定义,假设只有一个. ;
DIY: |
import java.util.Scanner;
public class CheckEmail{ public static void main(String args[]) { Scanner scan = new Scanner(System.in); String str = scan.nextLine(); if (str.length() < 5 || str.startsWith("@") || str.startsWith(".") || str.endsWith("@") || str.endsWith(".") || DotNum(str) > 1) { System.out.println("The format of email is wrong."); } else System.out.println("The format of email is correct."); }
public static int DotNum(String str) { int num = 0; for (int i = 0; i < str.length(); i++) { if (str.charAt(i) == '.') { num++; } } return num; } } |
wolex_li@hotmail.com The format of email is correct. |
Answers: |
public static void main(String args[]) { String str = "teatemail@gmail.com"; System.out.println(isEmail(str)); }
public static boolean isEmail(String email) { if (email == null || email.length() < 5) { // 长度肯定不够 return false; // 没有必要判断了 } if (!(email.contains("@")) && !(email.contains("."))) { return false; } if (email.startsWith("@") || email.startsWith(".") || email.endsWith("@") || email.endsWith(".")) { return false; } if (email.indexOf("@") > email.indexOf(".")) { return false; } return true; // 如果都正确则返回true } |
true |
注意比较自己写的代码和答案的,答案的明显可读性高,因为把if语句拆分来写,为了提高原来自己写的代码的可读性,可修改为:
package firstCourse;
import java.util.Scanner;
public class Hello { public static void main(String args[]) { Scanner scan = new Scanner(System.in); String str = scan.nextLine(); boolean flag = true; //增加一个信号灯 if (str.isEmpty() && str.length() < 5) { flag = false; } if (str.startsWith("@") || str.startsWith(".")) { flag = false; } if (str.endsWith("@") || str.endsWith(".")) { flag = false; } if (DotNum(str) > 1) { flag = false; } if (flag) { //最后再判断,这样代码的可读性更高 System.out.println("The format of email is correct."); } else System.out.println("The format of email is wrong."); }
public static int DotNum(String str) { int num = 0; for (int i = 0; i < str.length(); i++) { if (str.charAt(i) == '.') { num++; } } return num; } } |
因此,在开发中,要把复杂的或多个的if条件尽量简单化,if中的判断语句可以使用表达式(flag)来代替,让程序的每个结构看起来都显得简洁。
5、测试题
1、【Java】写出java中的数据类型划分及默认值
● 基本数据类型:
|- 数值型:
|- 整 形:byte、short、int、long; → 0
|- 浮点型:float、double; →0
|- 字符型:char; →空字符:'\u0000'
|- 布尔型:boolean; →false
● 引用数据类型:数组、类、接口 →null
2、【Java】写出&、&&和 |、|| 的区别
● &(普通与)和 |(普通或)指的是所有条件都进行判断;
● &&(短路与)如果前面的条件不满足,则后面不再进行判断,|| (短路或)如果前面的条件满足则后面不再判断;
● 在开发中为了性能的提高,主要用短路与和短路或操作;
● &和|除了用于逻辑运算之外,也可以进行位运算的操作;
3、【Java】完成一个数组的排序操作;
public class BubbleSort{ public static void main(String args[]){ int data[]={89,62,82,41,72,93,13,8}; for(int i=0;i<data.length-1;i++){ for(int j=0;j<data.length-i-1;j++){ if(data[j]>data[j+1]){ int temp=data[j]; data[j]=data[j+1]; data[j+1]=temp; } } } for(int i=0;i<data.length;i++){ System.out.print(data[i]+" "); } } } |
8 13 41 62 72 82 89 93 |
记得补充:也可以使用java.util.Arrays.Sort(数组名称)进行排序。