来源https://course.tianmaying.com/java-basic+java-environment
八、字符串操作
我们来比较以下三个字符串:
String str1;
String str2 = "";
String str3 = new String
str1
的值为null
,表示没有指向任何字符串对象str2
已经指向了一个字符串对象,这个对象的字符序列内容为空str3
是显式地调用构造函数创建字符串对象,该对象的字符序列内容为空,与第二行代码是基本一样的
String formatString = "我的名字是%s,我今年%d岁,我的爱好是%s";
String output = String.format(formatString, name, age, hobby);
System.out.println(output);
String formatString = "我的名字是%s,我今年%d岁,我的爱好是%s";
System.out.format(formatString, name, age, hobby);
StringBuffer
表示表示可变长的和可修改的字符序列。我们可以StringBuffer
进行插入或者追加字符序列、翻转字符序列等操作。
StringBuffer
必须通过构造函数进行初始化,它有三个构造方法:
StringBuffer()
:默认的构造方法预留16个字符的空间StringBuffer(int size)
:第二种形式接收一个整数参数,显示的设置缓冲区的大小StringBuffer(String str)
:第三种形式接收一个String
参数,设置StringBuffer
对象的初始内容,同时多预留16个字符的空间
当为一个类创建实例时,每个不同的实例的成员变量都有自己特定的值。
有时我们希望定义一个类成员,使其作为该类的公共成员,所有实例都共享该成员变量,此时需要使用static
关键字。
static
是Java语言中的修饰符,它可以修饰变量和方法。根据字面意思我们可以猜测static
是静态的意思,被它们修饰的变量或者方法就含有“静态”的性质,与“静态”对应的就是“实例”,因为“实例“都是程序在运行时动态生成的。
对于普通成员变量,每创建一个该类的实例就会创建该成员变量的一个拷贝,分配一次内存。由于成员变量是和类的实例绑定的,所以需要通过对象名进行访问,而不能直接通过类名对它进行访问。
而对于静态变量在内存中只有一份,Java虚拟机(JVM)只为静态变量分配一次内存,在加载类的过程中完成静态变量的内存分配。由于静态变量属于类,与类的实例无关,因而可以直接通过类名访问这类变量。
声明成static
的方法有几条限制:
- 仅能调用其他的
static
方法。 - 只能访问
static
数据。 - 不能以任何方式引用
this
或super
this
和
super
关键字。
类似于静态变量和静态方法,有static
修饰的代码块称为静态代码块。
它独立于类成员,可以有多个,JVM加载类的时候会执行这些静态代码块,如果static
代码块有多个,JVM则会按照它们在类中出现的顺序依次执行它们,且每个代码块只能执行一次。我们可以利用静态代码块可以对一些static
变量进行赋值。
十、泛型
在使用泛型时,我们可以把类型作为参数传入到泛型类中。类似于把参数传入到方法中一样。我们来实现一个通用的泛型Point类。
public class Point<T> {
private T x;
private T y;
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public T getY() {
return y;
}
public void setY(T y) {
this.y = y;
}
}
此时Point
成为了一个泛型类,T
是则是类型参数,T
具体是什么类型那要看程序运行的时候我们传入什么类型给他。
使用泛型类时,注意实际传入的类型参数不能是原生类型,必须是引用类型,因此如果希望传入int
类型的话,那么需要传入int
对应的包装类Interger
。对应地,double
类型则要传入包装类Double
。
泛型类支持多个类型参数。比方说我们需要实现一个三元组
Triple
类,存储三个类型的变量,我们可以实现如下:
public class Triple<A, B, C> {}
泛型也可以直接应用在一个方法中,不要求这个方法所属的类为泛型类。例如我们要获取一个对象的类名称:
public class Printer {
public static <T> void printArray(T[] objects) {
if (objects != null) {
for(T element : objects){
System.out.printf("%s",element);
}
}
}
public static void main(String[] args) {
Integer[] intArray = { 1, 2, 3, 4, 5 };
Character[] charArray = { 'T', 'I', 'A', 'N', 'M', 'A', 'Y', 'I', 'N', 'G' };
printArray(intArray);
printArray(charArray);
}
}
与泛型类不同的是泛型方法需要在方法返回值前用尖括号声明泛型类型名,这样才能在方法中使用这个标记作为返回值类型或参数类型。
十一、java集合
在Java 7和Java 8中,编译器能够根据变量声明时的泛型类型自动推断出实例化所用的泛型类型。
我们也可以使用迭代器Iterator
类来完成遍历。Iterator主要有两个方法,基于这两个方法就能进行遍历操作:
next()
方法来获取序列的下一个元素hasNext()
检查序列中是否还有元素
这两者的主要不同在于:
ArrayList
:通过下标随机访问元素快,但是插入、删除元素较慢LinkedList
:插入、删除和移动元素快,但是通过下标随机访问元素性能较低
如果你学过数据结构,就是知道ArrayList
是基于数组实现的,而LinkedList
是基于链表实现的。这两种数据结构的特点决定了这两个容器的不同之处。
Map
具有两个泛型参数,第一个是键的类型,第二个是值的类型。类型不能是原生类型,必须是引用类型,因此这里第一个类型是long
的包装类Long
。
使用Map
之后,查找和删除博客的实现就非常简单了:
put
方法可以增加一个键值对get
方法就能根据键获取到值remove
方法可以删除键对应的元素
Java编译器会在需要时自动帮我们进行原生数据类型和包装类之间进行转换,比如我们postMap.remove(id);
这条语句传入的是long
原生数据类型,Java编译器知道此时应该传入的是Long
,会自动帮我们做这个转换。
我们可以获取键、值或键值对的集合,分别使用keySet
, values
以及entrySet
。getAll
方法就通过postsMap.values()
获取所有的值,这里就是所有的Blog
对象。
注意这里调用了一个addAll
方法,这个方法是所有容器都有的一个方法,可以把另外一个容器中的元素加入其中。
for (Map.Entry<Long, Post> postEntry : postMap.entrySet()) {
Long id = postEntry.getKey();
Post post = postEntry.getValue();
// 在遍历中操作每一个post
...
}
Map.Entry类???
Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)。接口中有getKey(),getValue方法。
十二、封装与继承
封装是一种隐藏信息的技术,是将一个系统中的结构和行为通过类来划分的过程。即通过定义一组类,将特定的数据组合到某一个类中,形成一个整体,将该隐藏的数据进行保护,只对外暴露这些数据的访问的方法。
封装代码有两个好处:
- 代码使用者无需考虑实现细节就能直接使用它,同时不用担心不可预料的副作用,别人不能随便修改内部结构
- 在外部接口保持不变的情况下,自己可以修改内部的实现
封装甚至被一些面向对象的开发人员视为第一原则。
protected不能用来修饰类,只能修饰成员变量和成员函数。
继承是一种类和类之间的关系,是面向对象系统的基石。继承表明为一个"是一种"(is-a)的关系,为在现实中有很多这样的例子:学生是一种人;树是一种植物,矩形是一种图案。
我们可以把共性的结构和行为放到父类中,子类可以通过继承复用父类中的代码,并且根据自己的需要进行扩展。
在Java中,使用extends
关键字表示继承关系,来看一个具体的例子:
Java中的继承是单继承的,也就是说一个子类只能继承一个父类。子类会继承父类中的除构造函数以外的所有非private
成员方法,以及所有非private
成员变量。
生成子类对象或者实例时,Java默认地首先调用父类的不带参数的构造方法,接下来再调用子类的构造方法,生成子类对象。
this
表示对当前对象的引用,而super
表示对父类对象的引用。在子类的构造函数中,一般第一条语句是super();
,表示调用父类构造函数。也可以调用父类有参数的构造函数,比如super(name);
。
如果一个类的构造函数的第一语句既不是this()
也不是super()
时,就会隐含的调用super()
。
如果子类中有和父类中非private的同名方法,且返回类型和参数表也完全相同,就会覆盖从父类继承来的方法。
继承可以减少代码复用。继承可以建立抽象层次。
一个变量可以声明为final
,这样做的目的是阻止它的内容被修改。这意味着在声明final
变量的时候,必须初始化它(在这种用法上,final
类似于C/C++中的const
)。
通常情况下,我们会使用final
来定义一些常量。
final变量的所有字符选择大写是一个普遍的编码约定,用final修饰的变量在实例中不占用内存,它实质上是一个常数。
被final修饰的方法可以被子类继承,不能被子类的方法覆盖,因此,如果一个类不想让它的子类覆盖它的某个成员方法,就可以在该成员方法前面加上final
关键字
final不能修饰构造方法。由于父类中的private
成员方法是不能被子类覆盖的,所有有private
限制的成员方法默认也是final
的。
final
修饰方法除了不想让子类覆盖之外,还有一个原因就是高效,Java编译器在遇到
final
关键字修饰的方法时会使用内联机制,省去函数调用的开销,大大提高执行效率。
十三、抽象类与接口
在面向对象的领域一切都是对象,所有的对象都是通过类来描述的。如果我们要定义的一个类没有足够的信息来描述一个具体的对象,还需要其他的具体类来支持,这个时候我们可以考虑使用抽象类。在类定义的前面增加abstract
关键字,就表明一个类是抽象类。
抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。
abstract
关键字同样可以用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。抽象方法没有定义,方法名后面直接跟一个分号,而不是花括号。声明抽象方法会带来以下两个结果:
- 如果一个类包含抽象方法,那么该类必须是抽象类。
- 任何子类必须重写父类的抽象方法,否则就必须声明自身为抽象类
abstract
的,是因为它包含了没有具体实现的抽象方法。比如说我们给
Graph
类增加一个求面积的方法
area()
,因为我们不知道图形的形状,我们是无法给出实现的,只能交给特定的子类去实现,这时我们只能将
area()
声明为
abstract
的。
与抽象类类似的一个重要概念是接口。接口(Interface)是一组抽象方法的集合。接口中定义的方法没有方法体,它们以分号结束。
接口也和抽象类一样,无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。编写接口和编写类的方式是大体上是类似的,一个接口可以有多个方法,代码保存在以接口命名且以.java
结尾的文件中。接口使用interface
关键字进行定义。
比如我们定义一个Animal
接口。
interface Animal {
void eat();
void sleep();
}
这个接口包含了两个抽象方法:eat()
和sleep()
。接口中的方法都是外部可访问的,因此我们可以不需要用public
修饰。
接口中也可以声明变量,一般是final和static类型的,要以常量来初始化,实现接口的类不能改变接口中的变量。比如我们可以在Animal
接口增加一个成员变量TIMES_OF_EATING
,表示动物每天吃饭的次数。
接口访问权限有两种:public权限和默认权限,如果接口的访问权限是public的话,所有的方法和变量都是public。默认权限则同一个包内的类可以访问。
一个接口能继承另一个接口,和类之间的继承方式比较相似。接口的继承使用extends关键字,子接口继承父接口的方法。比如我们可以定义TerrestrialAnimal
接口,表示陆栖动物,它继承自Animal
接口,同时还具有run()
方法。
interface TerrestrialAnimal extends Animal {
void run();
}
类使用
implements
关键字实现接口。在类声明中,
implements
关键字放在
class
声明后面。接口支持多重继承,即一个类可以同时实现多个接口。
类需要对接口中的每一个方法都给出实现。
我们可以使用接口类型来声明一个变量,那么这个变量可以引用到一个实现该接口的对象。
讲集合的时候,我们说过ArrayList
是List
接口的实现,HashMap
是Map
接口的实现,这回你应该明白了吧。通过接口来申明变量,可以让程序更具有扩展性,因为将来我们更方便替换接口的实现。
相同点:
- 都不能被实例化
- 都包含抽象方法,这些抽象方法用于描述系统能提供哪些服务,而这些服务是由子类来提供实现的
- 在系统设计上,两者都代表系统的抽象层,当一个系统使用一棵继承树上的类时,应该尽量把引用变量声明为继承树的上层抽象类型,这样可以提高两个系统之间的松耦合
不同点:
- 在抽象类中可以为部分方法提供默认的实现,从而避免在子类中重复实现它们;但是抽象类不支持多继承。接口不能提供任何方法的实现,但是支持多继承。
- 接口代表了接口定义者和接口实现者的一种契约;而抽象类和具体类一般而言是一种is-a的关系,即两者在概念本质上是不同的。