Java面向对象基础

本文介绍了Java面向对象的一些基本知识,对面向对象的三大特征(继承封装多态)进行了讲解,另外还包括抽象类接口内部类String基础等知识。

类的定义方式

  • 每一个源文件必须有且只有一个public class,并且类名和文件名保持一致!
  • 一个Java文件可以同时定义多个class
1
2
3
4
5
6
7
8
9
// 每一个源文件必须有且只有一个public class,并且类名和文件名保持一致!
public class Car {
}
class Tyre { // 一个Java文件可以同时定义多个class
}
class Engine {
}
class Seat {
}

面向对象的内存分析

Java虚拟机的内存可以分为三个区域:栈 stack堆 heap方法区 method area

栈的特点如下:

  1. 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)
  2. JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)
  3. 栈属于线程私有,不能实现线程间的共享!
  4. 栈的存储特性是“先进后出,后进先出”
  5. 栈是由系统自动分配,速度快!栈是一个连续的内存空间!

堆的特点如下:

  1. 堆用于存储创建好的对象和数组(数组也是对象)
  2. JVM只有一个堆,被所有线程共享
  3. 堆是一个不连续的内存空间,分配灵活,速度慢!

方法区(又叫静态区)特点如下:

  1. JVM只有一个方法区,被所有线程共享!
  2. 方法区实际也是堆,只是用于存储类、常量相关的信息!
  3. 用来存放程序中永远是不变或唯一的内容。(类信息【Class对象】、静态变量、字符串常量等)
  • 内存分配图

构造方法

构造器也叫构造方法(constructor),用于对象的初始化。构造器是一个创建对象时被自动调用的特殊方法,目的是对象的初始化。构造器的名称应与类的名称一致。Java通过new关键字来调用构造器,从而返回该类的实例,是一种特殊的方法。

声明格式:

1
2
3
[修饰符] 类名(形参列表){
//n条语句
}

要点:

  1. 通过new关键字调用!!
  2. 构造器虽然有返回值,但是不能定义返回值类型(返回值的类型肯定是本类),不能在构造器里使用return返回某个值。
  3. 如果我们没有定义构造器,则编译器会自动定义一个无参的构造函数。如果已定义则编译器不会自动添加!
  4. 构造器的方法名必须和类名一致!
  5. 构造方法也是方法,只不过有特殊的作用而已。与普通方法一样,构造方法也可以重载。
  6. 如果方法构造中形参名与属性名相同时,需要使用this关键字区分属性与形参。

垃圾回收机制

Java引入了垃圾回收机制(Garbage Collection),令C++程序员最头疼的内存管理问题迎刃而解。Java程序员可以将更多的精力放到业务逻辑上而不是内存管理工作上,大大的提高了开发效率。

垃圾回收原理和算法

  1. 内存管理

Java的内存管理很大程度指的就是对象的管理,其中包括对象空间的分配和释放。

  • 对象空间的分配:使用new关键字创建对象即可
  • 对象空间的释放:将对象赋值null即可。垃圾回收器将负责回收所有”不可达”对象的内存空间。
  1. 垃圾回收过程

任何一种垃圾回收算法一般要做两件基本事情:

  • 发现无用的对象
  • 回收无用对象占用的内存空间。

垃圾回收机制保证可以将“无用的对象”进行回收。无用的对象指的就是没有任何变量引用该对象。Java的垃圾回收器通过相关算法发现无用对象,并进行清除和整理。

  1. 垃圾回收相关算法
  • 引用计数法

堆中每个对象都有一个引用计数。被引用一次,计数加1. 被引用变量值变为null,则计数减1,直到计数为0,则表示变成无用对象。优点是算法简单,缺点是“循环引用的无用对象”无法别识别。

示例:循环引用示例  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Student {
String name;
Student friend;

public static void main(String[] args) {
Student s1 = new Student();
Student s2 = new Student();

s1.friend = s2;
s2.friend = s1;
s1 = null;
s2 = null;
}
}

s1和s2互相引用对方,导致他们引用计数不为0,但是实际已经无用,但无法被识别。

  • 引用可达法(根搜索算法)

程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

开发中容易造成内存泄露的操作

如下四种情况时最容易造成内存泄露的场景,开发时一定注意:

  1. 创建大量无用对象

比如,我们在需要大量拼接字符串时,使用了String而不是StringBuilder。

1
2
3
4
String str = "";
for (int i = 0; i < 10000; i++) {
str += i; //相当于产生了10000个String对象
}
  1. 静态集合类的使用

像HashMap、Vector、List等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放。

  1. 各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭

IO流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网络连接,不使用的时候一定要关闭。

4.监听器的使用

释放对象时,没有删除相应的监听器。

要点:

  1. 程序员无权调用垃圾回收器。
  2. 程序员可以调用 System.gc(),该方法只是通知 JVM,并不是运行垃圾回收器。尽量少用,会申请启动Full GC,成本高,影响系统性能。
  3. finalize 方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用。

this关键字

对象创建的过程和this的本质

构造方法是创建 Java 对象的重要途径,通过new关键字调用构造器时,构造器也确实返回该类的对象,但这个对象并不是完全由构造器负责创建。创建一个对象分为如下四步:

  1. 分配对象空间,并将对象成员变量初始化为0或空
  2. 执行属性值的显示初始化
  3. 执行构造方法
  4. 返回对象的地址给相关的变量

this的本质就是“创建好的对象的地址”! 由于在构造方法调用前,对象已经创建。因此,在构造方法中也可以使用this代表“当前对象” 。

this最常的用法:

  1. 在程序中产生二义性之处,应使用this来指明当前对象;普通方法中,this总是指向调用该方法的对象。构造方法中,this总是指向正要初始化的对象。
  2. 使用this关键字调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句。
  3. this不能用于static方法中。

示例:this代表“当前对象”示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class User {
int id; //id
String name; //账户名
String pwd; //密码

public User() {
}
public User(int id, String name) {
System.out.println("正在初始化已经创建好的对象:"+this);
this.id = id; //不写this,无法区分局部变量id和成员变量id
this.name = name;
}
public void login(){
System.out.println(this.name+",要登录!"); //不写this效果一样
}

public static void main(String[] args) {
User u3 = new User(101,"高小七");
System.out.println("打印高小七对象:"+u3);
u3.login();
}
}

运行结果如下图所示。

示例:this()调用重载构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class TestThis {
int a, b, c;

TestThis() {
System.out.println("正要初始化一个Hello对象");
}
TestThis(int a, int b) {
// TestThis(); //这样是无法调用构造方法的!
this(); // 调用无参的构造方法,并且必须位于第一行!
a = a;// 这里都是指的局部变量而不是成员变量
// 这样就区分了成员变量和局部变量. 这种情况占了this使用情况大多数!
this.a = a;
this.b = b;
}
TestThis(int a, int b, int c) {
this(a, b); // 调用带参的构造方法,并且必须位于第一行!
this.c = c;
}

void sing() {
}
void eat() {
this.sing(); // 调用本类中的sing();
System.out.println("你妈妈喊你回家吃饭!");
}

public static void main(String[] args) {
TestThis hi = new TestThis(2, 3);
hi.eat();
}
}

static 关键字

在类中,用static声明的成员变量为静态成员变量,也称为类变量。 类变量的生命周期和类相同,在整个应用程序执行期间都有效。它有如下特点:

  1. 为该类的公用变量,属于类,被该类的所有实例共享,在类被载入时被显式初始化。
  2. 对于该类的所有对象来说,static成员变量只有一份。被该类的所有对象共享!!
  3. 一般用“类名.类属性/方法”来调用。(也可以通过对象引用或类名(不需要实例化)访问静态成员。)
  4. 在static方法中不可直接访问非static的成员。

核心要点:

  • static修饰的成员变量和方法,从属于类。
  • 普通变量和方法从属于对象的。

示例:static关键字的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 测试static关键字的用法
* @author admin
*
*/
public class User2 {
int id; // id
String name; // 账户名
String pwd; // 密码

static String company = "北京尚学堂"; // 公司名称


public User2(int id, String name) {
this.id = id;
this.name = name;
}

public void login() {
printCompany();
System.out.println(company);
System.out.println("登录:" + name);
}

public static void printCompany() {
// login();//调用非静态成员,编译就会报错
System.out.println(company);
}

public static void main(String[] args) {
User2 u = new User2(101, "高小七");
User2.printCompany();
User2.company = "北京阿里爷爷";
User2.printCompany();
}
}

运行结果如下图所示。

静态初始化块

构造方法用于对象的初始化!静态初始化块,用于类的初始化操作!在静态初始化块中不能直接访问非static成员。

注意事项:

静态初始化块执行顺序:

  1. 上溯到Object类,先执行Object的静态初始化块,再向下执行子类的静态初始化块,直到我们的类的静态初始化块为止。
  2. 构造方法执行顺序和上面顺序一样!

示例:static初始化块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class User3 {
int id; //id
String name; //账户名
String pwd; //密码
static String company; //公司名称
static {
System.out.println("执行类的初始化工作");
company = "北京尚学堂";
printCompany();
}
public static void printCompany(){
System.out.println(company);
}
public static void main(String[] args) {
User3 u3 = new User3();
}
}

执行结果如下图所示。

参数传值机制

Java中,方法中所有参数都是“值传递”,也就是“传递的是值的副本”。 也就是说,我们得到的是“原参数的复印件,而不是原件”。因此,复印件改变不会影响原件。

  • 基本数据类型参数的传值

传递的是值的副本。 副本改变不会影响原件。

  • 引用类型参数的传值

传递的是值的副本。但是引用类型指的是“对象的地址”。因此,副本和原参数都指向了同一个“地址”,改变“副本指向地址对象的值,也意味着原参数指向对象的值也发生了改变”。

示例:多个变量指向同一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 测试参数传值机制
* @author admin
*
*/
public class User4 {
int id; //id
String name; //账户名
String pwd; //密码

public User4(int id, String name) {
this.id = id;
this.name = name;
}

public void testParameterTransfer01(User4 u){
u.name="高小八";
}

public void testParameterTransfer02(User4 u){
u = new User4(200,"高三");
}

public static void main(String[] args) {
User4 u1 = new User4(100, "高小七");

u1.testParameterTransfer01(u1);
System.out.println(u1.name);

u1.testParameterTransfer02(u1);
System.out.println(u1.name);
}
}

执行结果如下图所示。

包(package)

包机制是Java中管理类的重要手段。 开发中,我们会遇到大量同名的类,通过包我们很容易对解决类重名的问题,也可以实现对类的有效管理。 包对于类,相当于文件夹对于文件的作用。

我们通过package实现对类的管理,package的使用有两个要点:

  1. 通常是类的第一句非注释性语句。

  2. 包名:域名倒着写即可,再加上模块名,便于内部管理类。

示例:package的命名举例

1
2
3
4
5
com.sun.test;
com.oracle.test;
cn.sxt.gao.test;
cn.sxt.gao.view;
cn.sxt.gao.view.model;

注意事项:

  1. 写项目时都要加包,不要使用默认包。
  2. com.gao和com.gao.car,这两个包没有包含关系,是两个完全独立的包。只是逻辑上看起来后者是前者的一部分。

示例:package的使用

1
2
3
4
5
6
package cn.sxt;
public class Test {
public static void main(String[] args) {
System.out.println("helloworld");
}
}

JDK中的主要包

Java中的常用包 说明
java.lang 包含一些Java语言的核心类,如String、Math、Integer、System和Thread,提供常用功能。
java.awt 包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。
java.net 包含执行与网络相关的操作的类。
java.io 包含能提供多种输入/输出功能的类。
java.util 包含一些实用工具类,如定义系统特性、使用与日期日历相关的函数。

导入类import

如果我们要使用其他包的类,需要使用import导入,从而可以在本类中直接通过类名来调用,否则就需要书写类的完整包名和类名。import后,便于编写代码,提高可维护性。

注意要点:

  1. Java会默认导入java.lang包下所有的类,因此这些类我们可以直接使用。
  2. 如果导入两个同名的类,只能用包名+类名来显示调用相关类: 
1
java.util.Date date  = new  java.util.Date();

示例:导入同名类的处理  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.sql.Date;
import java.util.*;//导入该包下所有的类。会降低编译速度,但不会降低运行速度。

public class Test{
public static void main(String[] args) {
//这里指的是java.sql.Date
Date now;
//java.util.Date因为和java.sql.Date类同名,需要完整路径
java.util.Date now2 = new java.util.Date();
System.out.println(now2);
//java.util包的非同名类不需要完整路径
Scanner input = new Scanner(System.in);
}
}

静态导入

静态导入(static import)是在JDK1.5新增加的功能,其作用是用于导入指定类的静态属性,这样我们可以直接使用静态属性。

示例:静态导入的使用  

1
2
3
4
5
6
7
8
9
10
11
package cn.sxt;
//以下两种静态导入的方式二选一即可
import static java.lang.Math.*;//导入Math类的所有静态属性
import static java.lang.Math.PI;//导入Math类的PI属性

public class Test2{
public static void main(String [] args){
System.out.println(PI);
System.out.println(random());
}
}

执行结果如下图所示。

继承

继承的实现

示例:使用extends实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Test{
public static void main(String[] args) {
Student s = new Student("高淇",172,"Java");
s.rest();
s.study();
}
}
class Person {
String name;
int height;
public void rest(){
System.out.println("休息一会!");
}
}
class Student extends Person {
String major; //专业
public void study(){
System.out.println("在尚学堂,学习Java");
}
public Student(String name,int height,String major) {
//天然拥有父类的属性
this.name = name;
this.height = height;
this.major = major;
}
}

instanceof 运算符

instanceof二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。比如:

示例:使用instanceof运算符进行类型判断

1
2
3
4
5
6
7
public class Test{
public static void main(String[] args) {
Student s = new Student("高淇",172,"Java");
System.out.println(s instanceof Person);
System.out.println(s instanceof Student);
}
}

两条语句的输出结果都是true。

继承使用要点

  1. 父类也称作超类基类派生类等。
  2. Java中只有单继承,没有像 C++ 那样的多继承。多继承会引起混乱,使得继承链过于复杂,系统难于维护。
  3. Java中没有多继承,接口有多继承。
  4. 子类继承父类,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。
  5. 如果定义一个类时,没有调用extends,则它的父类是:java.lang.Object

方法的重写override

子类通过重写父类的方法,可以用自身的行为替换父类的行为。方法的重写是实现多态的必要条件。

方法的重写需要符合下面的三个要点:

  1. “==”: 方法名、形参列表相同。
  2. “≤”:返回值类型和声明异常类型,子类小于等于父类。
  3. “≥”: 访问权限,子类大于等于父类。

示例:方法重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class TestOverride {
public static void main(String[] args) {
Vehicle v1 = new Vehicle();
Vehicle v2 = new Horse();
Vehicle v3 = new Plane();
v1.run();
v2.run();
v3.run();
v2.stop();
v3.stop();
}
}

class Vehicle { // 交通工具类
public void run() {
System.out.println("跑....");
}
public void stop() {
System.out.println("停止不动");
}
}
class Horse extends Vehicle { // 马也是交通工具
public void run() { // 重写父类方法
System.out.println("四蹄翻飞,嘚嘚嘚...");
}
}

class Plane extends Vehicle {
public void run() { // 重写父类方法
System.out.println("天上飞!");
}
public void stop() {
System.out.println("空中不能停,坠毁了!");
}
}

执行结果如下图所示:

Object类基本特性

Object类是所有Java类的根基类,也就意味着所有的Java对象都拥有Object类的属性和方法。如果在类的声明中未使用extends关键字指明其父类,则默认继承Object类。

示例:Object类

1
2
3
4
5
6
7
public class Person {
...
}
//等价于:
public class Person extends Object {
...
}

toString方法

Object类中定义有public String toString()方法,其返回值是 String 类型。Object类中toString方法的源码为:

1
2
3
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

根据如上源码得知,默认会返回“类名+@+16进制的hashcode”。在打印输出或者用字符串连接对象时,会自动调用该对象的toString()方法。

示例:toString()方法测试和重写toString()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
String name;
int age;
@Override
public String toString() {
return name+",年龄:"+age;
}
}
public class Test {
public static void main(String[] args) {
Person p=new Person();
p.age=20;
p.name="李东";
System.out.println("info:"+p);

Test t = new Test();
System.out.println(t);
}
}

执行结果如下图所示:

==和equals方法

“==”代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。

Object类中定义有:public boolean equals(Object obj)方法,提供定义“对象内容相等”的逻辑。比如,我们在公安系统中认为id相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。

Object 的 equals 方法默认就是比较两个对象的hashcode,是同一个对象的引用时返回 true 否则返回 false。但是,我们可以根据我们自己的要求重写equals方法。

示例:equals方法测试和自定义类重写equals方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class TestEquals { 
public static void main(String[] args) {
Person p1 = new Person(123,"高淇");
Person p2 = new Person(123,"高小七");
System.out.println(p1==p2); //false,不是同一个对象
System.out.println(p1.equals(p2)); //true,id相同则认为两个对象内容相同
String s1 = new String("尚学堂");
String s2 = new String("尚学堂");
System.out.println(s1==s2); //false, 两个字符串不是同一个对象
System.out.println(s1.equals(s2)); //true, 两个字符串内容相同
}
}
class Person {
int id;
String name;
public Person(int id,String name) {
this.id=id;
this.name=name;
}
public boolean equals(Object obj) {
if(obj == null){
return false;
}else {
if(obj instanceof Person) {
Person c = (Person)obj;
if(c.id==this.id) {
return true;
}
}
}
return false;
}
}

JDK提供的一些类,如String、Date、包装类等,重写了Object的equals方法,调用这些类的equals方法, x.equals (y) ,当x和y所引用的对象是同一类对象且属性内容相等时(并不一定是相同对象),返回 true 否则返回 false。

super关键字

定义及其使用

super是直接父类对象的引用。可以通过super来访问父类中被子类覆盖的方法或属性。

使用super调用普通方法,语句没有位置限制,可以在子类中随便调用。

若是构造方法的第一行代码没有显式的调用super(...)或者this(...);那么Java默认都会调用super(),含义是调用父类的无参数构造方法。这里的super()可以省略。

示例:super关键字的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestSuper01 { 
public static void main(String[] args) {
new ChildClass().f();
}
}
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);
System.out.println(super.value); //调用父类对象的成员变量
}
}

执行结果如下图所示:

继承树追溯

  • 属性/方法查找顺序:(比如:查找变量h)
  1. 查找当前类中有没有属性h
  2. 依次上溯每个父类,查看每个父类中是否有h,直到Object
  3. 如果没找到,则出现编译错误。
  4. 上面步骤,只要找到h变量,则这个过程终止。
  • 构造方法调用顺序:

构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上追溯到Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。

:静态初始化块调用顺序,与构造方法调用顺序一样,不再重复。

示例:构造方法向上追溯执行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestSuper02 { 
public static void main(String[] args) {
System.out.println("开始创建一个ChildClass对象......");
new ChildClass();
}
}
class FatherClass {
public FatherClass() {
System.out.println("创建FatherClass");
}
}
class ChildClass extends FatherClass {
public ChildClass() {
System.out.println("创建ChildClass");
}
}

执行结果如下图所示:

封装

需要让用户知道的才暴露出来,不需要让用户知道的全部隐藏起来,这就是封装。说的专业一点,封装就是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。

我们程序设计要追求“高内聚,低耦合”。 高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。

封装的优点

  1. 提高代码的安全性。
  2. 提高代码的复用性。
  3. “高内聚”:封装细节,便于修改内部代码,提高可维护性。
  4. “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。

示例:没有封装的代码会出现一些问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
String name;
int age;
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.name = "小红";
p.age = -45;//年龄可以通过这种方式随意赋值,没有任何限制
System.out.println(p);
}
}

我们都知道,年龄不可能是负数,也不可能超过130岁,但是如果没有使用封装的话,便可以给年龄赋值成任意的整数,这显然不符合我们的正常逻辑思维。执行结果如下图所示:

再比如说,如果哪天我们需要将Person类中的age属性修改为String类型的,你会怎么办?你只有一处使用了这个类的话那还比较幸运,但如果你有几十处甚至上百处都用到了,那你岂不是要改到崩溃。而封装恰恰能解决这样的问题。如果使用封装,我们只需要稍微修改下Person类的setAge()方法即可,而无需修改使用了该类的客户代码。

封装的实现

Java是使用“访问控制符”来控制哪些细节需要封装,哪些细节需要暴露的。 Java中4种“访问控制符”分别为privatedefaultprotectedpublic,它们说明了面向对象的封装性,所以我们要利用它们尽可能的让访问权限降到最低,从而提高安全性。

下面详细讲述它们的访问权限问题。其访问权限范围如下表所示。

修饰符 同一个类 同一个包 子类 所有类
private
default
protected
public
  1. private 表示私有,只有自己类能访问
  2. default表示没有修饰符修饰,只有同一个包的类能访问
  3. protected表示可以被同一个包的类以及其他包中的子类访问
  4. public表示可以被该项目的所有包中的所有类访问

下面做进一步说明Java中4种访问权限修饰符的区别:首先我们创建4个类:Person类、Student类、Animal类和Computer类,分别比较本类、本包、子类、其他包的区别。

  1. public访问权限修饰符:

public访问权限—本类中访问public属性:

public访问权限—本包中访问public属性:

public访问权限—不同包中的子类访问public属性:

public访问权限—不同包中的非子类访问public属性:

通过上四张图可以说明,public修饰符的访问权限为:该项目的所有包中的所有类。

  1. protected访问权限修饰符:将Person类中属性改为protected,其他类不修改。

protected访问权限—修改后的Person类:

protected访问权限—不同包中的非子类不能访问protected属性:

通过上两张图可以说明,protected修饰符的访问权限为:同一个包中的类以及其他包中的子类。

  1. 默认访问权限修饰符:将Person类中属性改为默认的,其他类不修改。

默认访问权限—修改后的Person类:

通过上图可以说明,默认修饰符的访问权限为:同一个包中的类。

  1. private访问权限修饰符:将Person类中属性改为private,其他类不修改。

private访问权限—修改后的Person类:

通过上图可以说明,private修饰符的访问权限为:同一个类。

封装的使用细节

类的属性的处理:

  1. 一般使用private访问权限。
  2. 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)。
  3. 一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰。

示例:JavaBean的封装实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Person {
// 属性一般使用private修饰
private String name;
private int age;
private boolean flag;
// 为属性提供public修饰的set/get方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean isFlag() {// 注意:boolean类型的属性get方法是is开头的
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}

下面我们使用封装来解决一下上面提到的年龄非法赋值的问题。

示例:封装的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Person {
private String name;
private int age;
public Person() {

}
public Person(String name, int age) {
this.name = name;
// this.age = age;//构造方法中不能直接赋值,应该调用setAge方法
setAge(age);
}

public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
//在赋值之前先判断年龄是否合法
if (age > 130 || age < 0) {
this.age = 18;//不合法赋默认值18
} else {
this.age = age;//合法才能赋值给属性age
}
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}

public class Test2 {
public static void main(String[] args) {
Person p1 = new Person();
//p1.name = "小红"; //编译错误
//p1.age = -45; //编译错误
p1.setName("小红");
p1.setAge(-45);
System.out.println(p1);

Person p2 = new Person("小白", 300);
System.out.println(p2);
}
}

执行结果如下图所示:

多态

多态指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。 比如:同样是调用人的“休息”方法,张三是睡觉,李四是旅游,高淇老师是敲代码,数学教授是做数学题; 同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。

多态的要点

  1. 多态是方法的多态,不是属性的多态(多态与属性无关)。
  2. 多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。
  3. 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。

示例:多态和类型转换测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Animal {
public void shout() {
System.out.println("叫了一声!");
}
}
class Dog extends Animal {
public void shout() {
System.out.println("旺旺旺!");
}
public void seeDoor() {
System.out.println("看门中....");
}
}
class Cat extends Animal {
public void shout() {
System.out.println("喵喵喵喵!");
}
}
public class TestPolym {
public static void main(String[] args) {
Animal a1 = new Cat(); // 向上可以自动转型
//传的具体是哪一个类就调用哪一个类的方法。大大提高了程序的可扩展性。
animalCry(a1);
Animal a2 = new Dog();
animalCry(a2);//a2为编译类型,Dog对象才是运行时类型。

//编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
// 否则通不过编译器的检查。
Dog dog = (Dog)a2;//向下需要强制类型转换
dog.seeDoor();
}

// 有了多态,只需要让增加的这个类继承Animal类就可以了。
static void animalCry(Animal a) {
a.shout();
}

/* 如果没有多态,我们这里需要写很多重载的方法。
* 每增加一种动物,就需要重载一种动物的喊叫方法。非常麻烦。
static void animalCry(Dog d) {
d.shout();
}
static void animalCry(Cat c) {
c.shout();
}*/
}

执行结果如下图所示:

上例给大家展示了多态最为多见的一种用法,即父类引用做方法的形参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。

由此,我们可以看出多态的主要优势是提高了代码的可扩展性,符合开闭原则。但是多态也有弊端,就是无法调用子类特有的功能,比如,我不能使用父类的引用变量调用Dog类特有的seeDoor()方法。

那如果我们就想使用子类特有的功能行不行呢?行!这就是我们下一章节所讲的内容:对象的转型

对象的转型

父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换

向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行强制类型转换,我们称之为向下转型!

示例:对象的转型

1
2
3
4
5
6
7
8
9
10
11
public class TestCasting {
public static void main(String[] args) {
Object obj = new String("北京尚学堂"); // 向上可以自动转型
// obj.charAt(0) 无法调用。编译器认为obj是Object类型而不是String类型
/* 编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
* 不然通不过编译器的检查。 */
String str = (String) obj; // 向下转型
System.out.println(str.charAt(0)); // 位于0索引位置的字符
System.out.println(obj == str); // true.他们俩运行时是同一个对象
}
}

执行结果如下图所示:

在向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类型转换异常ClassCastException。如下例所示。

示例:类型转换异常

1
2
3
4
5
6
7
8
public class TestCasting2 {
public static void main(String[] args) {
Object obj = new String("北京尚学堂");
//真实的子类类型是String,但是此处向下转型为StringBuffer
StringBuffer str = (StringBuffer) obj;
System.out.println(str.charAt(0));
}
}

执行结果如下图所示:

为了避免出现这种异常,我们可以使用instanceof运算符进行判断,如下例所示。

示例:向下转型中使用instanceof

1
2
3
4
5
6
7
8
9
10
11
12
public class TestCasting3 {
public static void main(String[] args) {
Object obj = new String("北京尚学堂");
if(obj instanceof String){
String str = (String)obj;
System.out.println(str.charAt(0));
}else if(obj instanceof StringBuffer){
StringBuffer str = (StringBuffer) obj;
System.out.println(str.charAt(0));
}
}
}

执行结果如下图所示:

final关键字

final关键字的作用

  1. 修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。
1
final  int   MAX_SPEED = 120;
  1. 修饰方法:该方法不可被子类重写。但是可以被重载!
1
final  void  study(){}
  1. 修饰类: 修饰的类不能被继承。比如:Math、String等。
1
final   class  A {}

final修饰方法如下图所示。

final修饰类如下图所示。

抽象方法和抽象类

  • 抽象方法

使用abstract修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。

  • 抽象类

包含抽象方法的类就是抽象类。通过abstract

示例:抽象类和抽象方法的基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//抽象类
abstract class Animal {
abstract public void shout(); //抽象方法
}
class Dog extends Animal {
//子类必须实现父类的抽象方法,否则编译错误
public void shout() {
System.out.println("汪汪汪!");
}
public void seeDoor(){
System.out.println("看门中....");
}
}
//测试抽象类
public class TestAbstractClass {
public static void main(String[] args) {
Dog a = new Dog();
a.shout();
a.seeDoor();
}
}

抽象类的使用要点:

  1. 有抽象方法的类只能定义成抽象类
  2. 抽象类不能实例化,即不能用new来实例化抽象类。
  3. 抽象类可以包含属性、方法、构造方法。但是构造方法不能用来new实例,只能用来被子类调用。
  4. 抽象类只能用来被继承。
  5. 抽象方法必须被子类实现。

接口

接口的定义和使用

声明格式

1
2
3
4
[访问修饰符]  interface 接口名   [extends  父接口1,父接口2…]  {
常量定义;
方法定义;
}

定义接口的详细说明

  1. 访问修饰符:只能是public或默认。
  2. 接口名:和类名采用相同命名机制。
  3. extends:接口可以多继承。
  4. 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。
  5. 方法:接口中的方法只能是:public abstract。 省略的话,也是public abstract。

要点

  1. 子类通过implements来实现接口中的规范。
  2. 接口不能创建实例,但是可用于声明引用变量类型。
  3. 一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。
  4. JDK1.7之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。
  5. JDK1.8后,接口中包含普通的静态方法。

示例:接口的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class TestInterface {
public static void main(String[] args) {
Volant volant = new Angel();
volant.fly();
System.out.println(Volant.FLY_HIGHT);

Honest honest = new GoodMan();
honest.helpOther();
}
}
/**飞行接口*/
interface Volant {
int FLY_HIGHT = 100; // 总是:public static final类型的;
void fly(); //总是:public abstract void fly();
}
/**善良接口*/
interface Honest {
void helpOther();
}
/**Angle类实现飞行接口和善良接口*/
class Angel implements Volant, Honest{
public void fly() {
System.out.println("我是天使,飞起来啦!");
}
public void helpOther() {
System.out.println("扶老奶奶过马路!");
}
}
class GoodMan implements Honest {
public void helpOther() {
System.out.println("扶老奶奶过马路!");
}
}
class BirdMan implements Volant {
public void fly() {
System.out.println("我是鸟人,正在飞!");
}
}

执行结果如下图所示:

接口的多继承

接口完全支持多继承。和类的继承类似,子接口扩展某个父接口,将会获得父接口中所定义的一切。

示例:接口的多继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface A {
void testa();
}
interface B {
void testb();
}
/**接口可以多继承:接口C继承接口A和B*/
interface C extends A, B {
void testc();
}
public class Test implements C {
public void testc() {
}
public void testa() {
}
public void testb() {
}
}

内部类

内部类介绍

一般情况,我们把类定义成独立的单元。有些情况下,我们把一个类放在另一个类的内部定义,称为内部类(innerclasses)

内部类可以使用public、default、protected 、private以及static修饰。而外部顶级类(我们以前接触的类)只能使用public和default修饰。

注意:

内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。

示例:内部类介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**外部类Outer*/
class Outer {
private int age = 10;
public void show(){
System.out.println(age);//10
}
/**内部类Inner*/
public class Inner {
//内部类中可以声明与外部类同名的属性与方法
private int age = 20;
public void show(){
System.out.println(age);//20
}
}
}

上例编译后会产生两个不同的字节码文件,如下图所示:

内部类的作用

  1. 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。
  2. 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。 但外部类不能访问内部类的内部属性。
  3. 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整。

内部类的使用场合

  1. 由于内部类提供了更好的封装特性,并且可以很方便的访问外部类的属性。所以,在只为外部类提供服务的情况下可以优先考虑使用内部类。
  2. 使用内部类间接实现多继承:每个内部类都能独立地继承一个类或者实现某些接口,所以无论外部类是否已经继承了某个类或者实现了某些接口,对于内部类没有任何影响。

内部类的分类

在Java中内部类主要分为成员内部类非静态内部类静态内部类)、匿名内部类局部内部类

成员内部类

可以使用private、default、protected、public任意进行修饰。

类文件:外部类$内部类.class

非静态内部类

外部类里使用非静态内部类和平时使用其他类没什么不同

  1. 非静态内部类必须寄存在一个外部类对象里。因此,如果有一个非静态内部类对象那么一定存在对应的外部类对象。非静态内部类对象单独属于外部类的某个对象。

  2. 非静态内部类可以直接访问外部类的成员,但是外部类不能直接访问非静态内部类成员。

  3. 非静态内部类不能有静态方法、静态属性和静态初始化块。

  4. 外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例。

  5. 成员变量访问要点:

    • 内部类里方法的局部变量:变量名。
    • 内部类属性:this.变量名。
    • 外部类属性:外部类名.this.变量名。

示例:成员变量的访问要点

1
2
3
4
5
6
7
8
9
10
11
12
class Outer {
private int age = 10;
class Inner {
int age = 20;
public void show() {
int age = 30;
System.out.println("内部类方法里的局部变量age:" + age);// 30
System.out.println("内部类的成员变量age:" + this.age);// 20
System.out.println("外部类的成员变量age:" + Outer.this.age);// 10
}
}
}
  1. 内部类的访问:
  • 外部类中定义内部类:
1
new Inner()
  • 外部类以外的地方使用非静态内部类:
1
Outer.Inner  varname = new Outer().new Inner()。

示例:内部类的访问

1
2
3
4
5
6
7
8
9
10
public class TestInnerClass {
public static void main(String[] args) {
//先创建外部类实例,然后使用该外部类实例创建内部类实例
Outer.Inner inner = new Outer().new Inner();
inner.show();
Outer outer = new Outer();
Outer.Inner inn = outer.new Inner();
inn.show();
}
}

执行结果如下图所示:

静态内部类

  1. 定义方式:
1
2
3
static  class   ClassName {
//类体
}
  1. 使用要点:
  • 当一个静态内部类对象存在,并不一定存在对应的外部类对象。 因此,静态内部类的实例方法不能直接访问外部类的实例方法。
  • 静态内部类看做外部类的一个静态成员。 因此,外部类的方法中可以通过:“静态内部类.名字”的方式访问静态内部类的静态成员,通过 new 静态内部类()访问静态内部类的实例。

示例:静态内部类的访问

1
2
3
4
5
6
7
8
9
10
11
class Outer{
//相当于外部类的一个静态成员
static class Inner{
}
}
public class TestStaticInnerClass {
public static void main(String[] args) {
//通过 new 外部类名.内部类名() 来创建内部类对象
Outer.Inner inner =new Outer.Inner();
}
}

匿名内部类

适合那种只需要使用一次的类。比如:键盘监听操作等等。

语法:

1
2
3
new  父类构造器(实参类表) \实现接口 () {
//匿名内部类类体!
}

示例:匿名内部类的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
this.addWindowListener(new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
}
);
this.addKeyListener(new KeyAdapter(){
@Override
public void keyPressed(KeyEvent e) {
myTank.keyPressed(e);
}
@Override
public void keyReleased(KeyEvent e) {
myTank.keyReleased(e);
}
}
);

注意:

  1. 匿名内部类没有访问修饰符。
  2. 匿名内部类没有构造方法。因为它连名字都没有那又何来构造方法呢。

局部内部类

还有一种内部类,它是定义在方法内部的,作用域只限于本方法,称为局部内部类。

局部内部类的的使用主要是用来解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类。局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法中被使用,出了该方法就会失效。

局部内部类在实际开发中应用很少。

示例:方法中的内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test2 {
public void show() {
//作用域仅限于该方法
class Inner {
public void fun() {
System.out.println("helloworld");
}
}
new Inner().fun();
}
public static void main(String[] args) {
new Test2().show();
}
}

执行结果如下图所示:

String基础

基础知识

  1. String类又称作不可变字符序列。
  2. String位于java.lang包中,Java程序默认导入java.lang包下的所有类。
  3. Java字符串就是Unicode字符序列,例如字符串“Java”就是4个Unicode字符’J’、’a’、’v’、’a’组成的。
  4. Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义的类String,每个用双引号括起来的字符串都是String类的一个实例。

示例:String类的实例

1
2
String e = ""  ; // 空字符串
String greeting = " Hello World ";
  1. Java允许使用符号”+”把两个字符串连接起来。

示例:字符串连接

1
2
3
String s1 = "Hello";
String s2 = "World! ";
String s = s1 + s2; //HelloWorld!
  • 符号”+”把两个字符串按给定的顺序连接在一起,并且是完全按照给定的形式。
  • 当”+”运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串然后再进行连接。

示例:”+”连接符

1
2
3
4
int age = 18;
String str = "age is" + age; //str赋值为"age is 18"
//这种特性通常被用在输出语句中:
System.out.println("age is" + age);

String类和常量池

在Java的内存分析中,我们会经常听到关于“常量池”的描述,实际上常量池也分了以下三种:

  1. 全局字符串常量池(String Pool)

全局字符串常量池中存放的内容是在类加载完成后存到String Pool中的,在每个VM中只有一份,存放的是字符串常量的引用值(在堆中生成字符串对象实例)。

  1. class文件常量池(Class Constant Pool)

class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量(文本字符串、final常量等)和符号引用。

  1. 运行时常量池(Runtime Constant Pool)

运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

示例:常量池

1
2
3
4
5
6
7
8
String str1 = "abc";
String str2 = new String("def");
String str3 = "abc";
String str4 = str2.intern();
String str5 = "def";
System.out.println(str1 == str3);// true
System.out.println(str2 == str4);// false
System.out.println(str4 == str5);// true

上例中经过编译之后,在该类的class常量池中存放一些符号引用,然后类加载之后,将class常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的“abc”实例对象),然后将这个对象的引用存到全局String Pool中,也就是String Pool中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询String Pool,保证String Pool里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。

回到上例的程序,现在就很容易解释整个程序的内存分配过程了,首先,在堆中会有一个“abc”实例,全局String Pool中存放着“abc”的一个引用值,然后在运行第二句的时候会生成两个实例,一个是“def”的实例对象,并且String Pool中存储一个“def”的引用值,还有一个是new出来的一个“def”的实例对象,与上面那个是不同的实例,当在解析str3的时候查找String Pool,里面有“abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同,str4是在运行的时候调用intern()函数,返回String Pool中“def”的引用值,如果没有就将str2的引用值添加进去,在这里,String Pool中已经有了“def”的引用值了,所以返回上面在new str2的时候添加到String Pool中的 “def”引用值,最后str5在解析的时候就也是指向存在于String Pool中的“def”的引用值,那么这样一分析之后,结果就容易理解了。

String类常用的方法

方法 解释说明
char charAt(int index) 返回字符串中第index个字符
boolean equals(String other) 如果字符串与other相等,返回true;否则,返回false。
boolean equalsIgnoreCase(String other) 如果字符串与other相等(忽略大小写),则返回true;否则,返回false。
int indexOf(String str) 返回从头开始查找第一个子字符串str在字符串中的索引位置。如果未找到子字符串str,则返回-1。
lastIndexOf() 返回从末尾开始查找第一个子字符串str在字符串中的索引位置。如果未找到子字符串str,则返回-1。
int length() 返回字符串的长度。
String replace(char oldChar,char newChar) 返回一个新串,它是通过用newChar替换此字符串中出现的所有oldChar而生成的。
boolean startsWith(String prefix) 如果字符串以prefix开始,则返回true。
boolean endsWith(String prefix) 如果字符串以prefix结尾,则返回true。
String substring(int beginIndex) 返回一个新字符串,该串包含从原始字符串beginIndex到串尾。
String substring(int beginIndex,int endIndex) 返回一个新字符串,该串包含从原始字符串beginIndex到串尾或endIndex-1的所有字符。
String toLowerCase() 返回一个新字符串,该串将原始字符串中的所有大写字母改成小写字母。
String toUpperCase() 返回一个新字符串,该串将原始字符串中的所有小写字母改成大写字母。
String trim() 返回一个新字符串,该串删除了原始字符串头部和尾部的空格。

示例:String类常用方法一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StringTest1 {
public static void main(String[] args) {
String s1 = "core Java";
String s2 = "Core Java";
System.out.println(s1.charAt(3));//提取下标为3的字符
System.out.println(s2.length());//字符串的长度
System.out.println(s1.equals(s2));//比较两个字符串是否相等
System.out.println(s1.equalsIgnoreCase(s2));//比较两个字符串(忽略大小写)
System.out.println(s1.indexOf("Java"));//字符串s1中是否包含Java
System.out.println(s1.indexOf("apple"));//字符串s1中是否包含apple
String s = s1.replace(' ', '&');//将s1中的空格替换成&
System.out.println("result is :" + s);
}
}

执行结果如下图所示:

示例:String类常用方法二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class StringTest2 {
public static void main(String[] args) {
String s = "";
String s1 = "How are you?";
System.out.println(s1.startsWith("How"));//是否以How开头
System.out.println(s1.endsWith("you"));//是否以you结尾
s = s1.substring(4);//提取子字符串:从下标为4的开始到字符串结尾为止
System.out.println(s);
s = s1.substring(4, 7);//提取子字符串:下标[4, 7) 不包括7
System.out.println(s);
s = s1.toLowerCase();//转小写
System.out.println(s);
s = s1.toUpperCase();//转大写
System.out.println(s);
String s2 = " How old are you!! ";
s = s2.trim();//去除字符串首尾的空格。注意:中间的空格不能去除
System.out.println(s);
System.out.println(s2);//因为String是不可变字符串,所以s2不变
}
}

执行结果如下图所示:

字符串相等的判断

  1. equals方法用来检测两个字符串内容是否相等。如果字符串s和t内容相等,则s.equals(t)返回true,否则返回false。
  2. 要测试两个字符串除了大小写区别外是否是相等的,需要使用equalsIgnoreCase方法。
  3. 判断字符串是否相等不要使用”==”。

示例:忽略大小写的字符串比较

1
"Hello".equalsIgnoreCase("hellO");//true

示例:字符串的比较”==”与equals()方法

1
2
3
4
5
6
7
8
9
10
public class TestStringEquals {
public static void main(String[] args) {
String g1 = "北京尚学堂";
String g2 = "北京尚学堂";
String g3 = new String("北京尚学堂");
System.out.println(g1 == g2); // true 指向同样的字符串常量对象
System.out.println(g1 == g3); // false g3是新创建的对象
System.out.println(g1.equals(g3)); // true g1和g3里面的字符串内容是一样的
}
}

执行结果如下图所示:

上例的内存分析如下图所示:

开闭原则

开闭原则(Open-Closed Principle)就是让设计的系统对扩展开放,对修改封闭。

  • 对扩展开放:

就是指,应对需求变化要灵活。 要增加新功能时,不需要修改已有的代码,增加新代码即可。

  • 对修改封闭:

就是指,核心部分经过精心设计后,不再因为需求变化而改变。

在实际开发中,我们无法完全做到,但应尽量遵守开闭原则。

模板方法模式和回调机制

模板方法模式很常用,其目的是在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。在标准的模板方法模式实现中,主要是使用继承的方式,来让父类在运行期间可以调用到子类的方法。 详见抽象类部分示例。

其实在Java开发中,还有另外一个方法可以实现同样的功能,那就是Java回调技术。回调是一种双向的调用模式,也就是说,被调用的接口被调用时也会调用对方的接口,简单点说明就是:A类中调用B类中的C方法,然后B类中的C方法中反过来调用A类中的D方法,那么D这个方法就叫回调方法。

回调的具体过程如下

  1. Class A实现接口CallBack —— 背景1
  2. class A中包含class B的引用 ——背景2
  3. class B有一个参数为CallBack的方法C ——背景3
  4. 前三条是我们的准备条件,接下来A的对象调用B的方法C
  5. 然后class B就可以在C方法中调用A的方法D

这样说大家可能还是不太理解,下面我们根据下例来说明回调机制。该示例的生活背景为:有一天小刘遇到一个很难的问题“学习Java选哪家机构呢?”,于是就打电话问小高,小高一时也不太了解行情,就跟小刘说,我现在还有事,等忙完了给你咨询咨询,小刘也不会傻傻的拿着电话去等小高的答案,于是小刘对小高说,先挂电话吧,你知道答案后再打我电话告诉我吧,于是挂了电话。小高先去办自己的事情去了,过了几个小时,小高打电话给小刘,告诉他答案是“学Java当然去……”。

示例:回调机制示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/** 
* 回调接口
*/
interface CallBack {
/**
* 小高知道答案后告诉小刘时需要调用的方法,即回调方法
* @param result 是问题的答案
*/
public void answer(String result);
}
/**
* 小刘类:实现了回调接口CallBack(背景一)
*/
class Liu implements CallBack {
/**
* 包含小高对象的引用 (背景二)
*/
private Gao gao;

public Liu(Gao gao){
this.gao = gao;
}

/**
* 小刘通过这个方法去问小高
* @param question 小刘问的问题“学习Java选哪家机构呢?”
*/
public void askQuestion(String question){
//小刘问小高问题
gao.execute(Liu.this, question);
}
/**
* 小高知道答案后调用此方法告诉小刘
*/
@Override
public void answer(String result) {
System.out.println("小高告诉小刘的答案是:" + result);
}
}
/**
* 小高类
*/
class Gao {
/**
* 相当于class B有一个参数为CallBack的方法C(背景三)
*/
public void execute(CallBack callBack, String question){
System.out.println("小刘问的问题是:" + question);
//模拟小高挂点后先办自己的事情花了很长时间
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//小高办完自己的事情后想到了答案
String result = "学Java当然去......";
//小高打电话把答案告诉小刘,相当于class B 反过来调用class A 的D方法
callBack.answer(result);
}
}

public class Test {
public static void main(String[] args) {
Gao gao= new Gao();
Liu liu = new Liu(gao);
//小刘问问题
liu.askQuestion("学习Java选哪家机构呢?");
}
}

执行结果如下图所示:

通过回调在接口中定义的方法,调用到具体的实现类中的方法,其本质是利用Java的动态绑定技术,在这种实现中,可以不把实现类写成单独的类,而使用内部类或匿名内部类来实现回调方法。

组合模式

组合模式是将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

示例:对象的组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Cpu {
public void run() {
System.out.println("quickly.........");
}
}
class MainBoard {
public void connect() {
System.out.println("connect...........");
}
}
class Memory {
public void store() {
System.out.println("store........");
}
}
public class Computer {
Cpu cpu;
Memory memory;
MainBoard mainBoard;

public void work() {
cpu.run();
memory.store();
mainBoard.connect();
}

public static void main(String[] args) {
Computer computer = new Computer();
computer.cpu = new Cpu();
computer.mainBoard = new MainBoard();
computer.memory = new Memory();
computer.work();
}
}

执行结果如下图所示:

感谢您的支持!