[Java读书笔记] Effective Java(Third Edition) 第2章 创建和销毁对象
第 1 条:用静态工厂方法代替构造器
对于类而言,获取一个实例的方法,传统是提供一个共有的构造器。
类可以提供一个公有静态工厂方法(static factory method), 它只是一个返回类的实例的静态方法。
示例:Boolean的装箱类,将boolean基本类型值转换成一个Boolean对象引用
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
静态工厂方法与构造器不同的优点:
第一大优势:有名称。更容易使用,更容易阅读理解。
第二大优势:不必每次调用它们的时候都创建一个新对象。
第三大优势:它们可以返回类型的任何子类型的对象。
例如Java Collections Framework的集合接口45个工具实现,提供了不可修改的集合、同步集合等等。几乎所有都是通过静态工厂方法在一个不可实例化的类(java.util.Collections)中导出。
第四大优势:所有返回对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。
第五大优势: 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。
静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础。例如JDBC(Java数据库连接) API.
静态工厂方法缺点:
第一: 类如果不含共有的或受保护的构造器,就不能被子类化。
第二:程序员很难发现他们。在API文档中没有明确标识出来。
静态工厂方法一些惯用名称:
from:
Date d = Date.from(instant);
of:
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf:
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance或getInstance:
StackWaler luke = StackWaler.getInstance(options);
create或newInstance:
Object newArray = Array.newInstance(classObject, arrayLen);
getType:
FileStore fs = Files.getFileStore(path);
newType:
BufferReader br = Files.newBufferReader(path);
type(getType和newType的简化版):
List<Complaint> litany = Collections.list(legacyLitany);
总结,静态工厂方法和共有构造器各有用处,需要理解各自长处。静态工厂方法经常更加合适,因此切忌第一反应是提供公有构造器,而不是考虑静态工厂方法。
第 2 条:遇到多个构造器参数时要考虑使用构建器
静态工厂和构造器有个共同局限性:都不能很好地扩张到大量的可选参数。
第一种方案(一般解决方法):重叠构造器(telescoping constructor)模式:
提供第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有二个可选参数,以此类推,最后一个构造器包含所有可选参数。
缺点:重叠构造器模式可行,但是有很多参数时,代码很难编写,难以阅读。
第二种方案:JavaBeans模式:
先调用一个无参数构造器来创建对象,然后再调用setter方法来设置每个必要参数和每个可选参数。
缺点:在构造过程中,JavaBeans可能处于不一致的状态。(线程不安全) 另外一点是这种模式就不能把类做成不可变的。
第三种方案:建造者(builder)模式:
例子:
public class NutritionFacts { private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate; public static class Builder {
// 必须属性
private final int servingSize;
private final int servings;
// 可选属性
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0; public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
} public Builder setCalories(int calories) {
this.calories = calories;
return this;
} public Builder setFat(int fat) {
this.fat = fat;
return this;
} public Builder setSodium(int sodium) {
this.sodium = sodium;
return this;
} public Builder setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
return this;
} public NutritionFacts build() {
return new NutritionFacts(this);
}
} private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
使用代码:
NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();
Builder模式便于编写和阅读。
Builder模式也适用于类层次结构。例子:抽象类Pizza, 两个子类,一个NyPizza表示经典纽约风味披萨,一个Calzone表示馅料内置半月形披萨。(代码略)
使用代码:
NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addToping(ONION).build();
Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();
总结: 如果类的构造器或者静态工厂中具有多个参数,Builder模式是一种很好的选择,特别是当大多数参数是可选参数。
第 3 条:使用私有构造器或枚举类型来强制实现 singleton 属性
单例(singleton)就是一个只实例化一次的类。通常用于代表一个无状态的对象,比如函数,或者本质上是唯一的系统组件。
使类成为Singleton会使它的客户端测试变得十分困难。
两种常见实现方式:
1. Public final field: 公有静态成员是一个final域:
// Singleton with public final field
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
该方法的优点:
- API 明确表示这个类就是一个单例。公共静态属性是 final 的,所以它总是包含相同的对象引用。
- 这个方法很简单。
2. Singleton with static factory 公有成员是一个静态工厂方法:
// Singleton with static factory
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() { ... }
}
该方法的优点:
- 可以方便地将类的实现改为非单例,并且用户代码不需要改变。
- 可以编写一个泛型单例工厂。
- 方法引用可以被用作 supplier,例如 Elvis::instance 等同于 Supplier<Elvis>。
为了实现Singleton类变成可序列化的(Serializable), 需要2点:
1. 声明中加implements Serializable。
2. 必须声明所有实例域都是瞬时的(transient),并提供一个readResolve方法。
否则,每次反序列化一个序列化的实例时,都会创建一个新实例。
// readResolve method to preserve singleton property
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
第三种实现方法:声明一个包含单个元素的枚举类型:
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
这种方法在功能上与公有域方法相似,更加简洁。无偿地提供序列化机制,绝对防止多次实例化。
总结:单元素的枚举类型经常成为实现Singleton的最佳方法。
第 4 条:通过私有构造器强化不可实例化的能力
有时候可能需要编写只包含静态方法和静态域的类。比如java.lang.Math或者java.util.Arrays。
还有java.util.Collections的方式,吧实现特定接口的对象上的静态方法,包括工厂方法组织起来。
企图通过将类做成抽象类来强制该类不可被实例化是行不通的。
该类可以被子类化,并且该子类也可以被实例化。
只有让这个类包含一个私有构造器,他才不能被实例化。
副作用:使得一个类不能被子类化(不能被继承)。所有的构造器都必须显式或隐式地调用超类(superclass)构造器,
而子类就没有可以访问的超类的构造器可调用了。
第 5 条:优先考虑依赖注入来引入资源
有许多类会依赖一个或者多个底层的资源。例如,拼写检查器需要依赖一个或多个词典。
静态工具类和Singleton类不适合于需要引用底层资源的类。
这里需要能够支持类的多个实例,每一个实例都使用客户端指定的资源(本例中的词典)。
满足该条件的模式是:当创建一个新的实例时,就将该资源传到构造器中。(依赖注入的一种形式)
// Dependency injection provides flexibility and testability
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
总之,不要用Singleton和静态工厂类来实现依赖一个或者多个底层资源的类,且该资源的行为会影响到该类的行为,也不要直接用这个类来创建这些资源。
而应该讲这些资源或者工厂传给构造器(或者静态工厂,构建器),通过它们来创建类。这就是依赖注入。
第 6 条:避免创建不必要的对象
通常来讲,重用一个对象比创建一个功能相同的对象更加合适。
重用速度更快,并且更接近现代的代码风格。如果对象是不可变的(immutable)(条款 17),它总是可以被重用。
第一种多余创建对象的场景:String字符串:
String s = new String("bikini"); // DON'T DO THIS!
这个语句每次执行时都会创建一个新的 String 实例,而这些实例的创建都是不必要的。
如果这种用法发生在循环或者频繁调用的方法中,就会创建数百万个毫无必要的 String 实例。
改进后:
String s = "bikini";
第二种多余创建对象的场景:正则表达式:
写一个方法来确定一个字符串是否是一个合法的罗马数字:
// Performance can be greatly improved! static boolean isRomanNumeral(String s) { return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); }
这个实现的问题在于它依赖于 String.matches 方法。
虽然 String.matches 是检查字符串是否与正则表达式匹配的最简单方法,但它不适合在性能临界的情况下重复使用。
因为它在内部为正则表达式创建一个 Pattern 实例,并且只使用一次,之后这个 Pattern 实例就会被 JVM 进行垃圾回收。
创建 Pattern 实例是昂贵的,因为它需要将正则表达式编译成有限状态机(finite state machine)。
为了提高性能,将正则表达式显式编译为一个 Pattern 实例(不可变)并且缓存它,在 isRomanNumeral 方法的每个调用中重复使用相同的实例:
// Reusing expensive object for improved performance
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
第三种多余创建对象的场景:自动装箱(autoboxing)
它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。
// Hideously slow! Can you spot the object creation? private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
这个程序的结果是正确的,但由于写错了一个字符,运行的结果要比实际慢很多。
变量 sum 被声明成了 Long 而不是 long ,这意味着程序构造了大约 2^31 个不必要的Long实例(每次往 Long 类型的 sum 变量中增加一个 long 类型的 i)。
把 sum 变量的类型由 Long 改为 long 会使性能得到很大提升。这个教训很明显:优先使用基本类型而不是包装的基本类型,也要注意无意识的自动装箱。
这个条目不应该被误解为暗示对象创建是昂贵的,应该避免创建对象。
相反,创建和回收小的对象非常廉价,构造器只会做很少的工作,尤其在现代 JVM 实现上。 创建额外的对象以增强程序的清晰性,简单性或功能性通常是件好事。
反之,通过维护自己的对象池来避免创建对象并不是一个好的做法,除非池中的对象是非常重量级的。正确使用对象池的典型对象示例是数据库连接池。
第 7 条:消除过期对象的引用
Java语言,当你用完对象之后,他们会被自动回收。它很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情了,其实不然。
// Can you spot the "memory leak"?
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
} public void push(Object e) {
ensureCapacity();
elements[size++] = e;
} public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
} /**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
这个程序没有什么明显的错误,但有一个潜在的问题——“内存泄漏”。
由于垃圾回收器的活动的增加,或内存占用的增加,程序的性能会下降。极端情况下导致程序失败(OutOfMemoryError)错误。
程序中哪里发生了内存泄漏?
如果一个栈先增长,后收缩,那么从栈弹出的对象不会被当作垃圾回收掉,即使使用栈的程序不再引用这些对象。
这是因为栈内部维护了对这些对象的过期引用( obsolete references)。 过期引用简单来说就是永远不会再一次被解引用的引用。
在上面这段代码中,数组“活动部分(active portion)”之外的任何引用都是过期的。活动部分是由索引下标小于 size 的那些元素组成的。
这类问题的解决方法很简单:一旦对象引用过期,只需清空这些引用(将它们设置为 null)。
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
清空对象引用应该是一种例外,而不是一种规范。消除过期引用的最好方法是让包含引用的变量结束其生命周期。
常见的内存泄漏场景:
1. 只要类是自己管理内存,程序员就应该警惕内存泄漏问题。一旦元素被释放,则该元素中包含的任何对象引用都应该被清空。
2. 内存泄漏的另一个常见来源是缓存。一旦将对象引用放入缓存中,很容易忘记它的存在,并且在它变得无关紧要之后,仍然保留在缓存中。
常见解决方案之一,用WeakHashMap代表缓存,当缓存中的项过期之后,它们就会被自动删除。
常见解决方案之二,缓存应该时不时的清除掉没用的项。清除工作可以由后台线程(ScheduledThreadPoolExecutor)来完成。
3. 监听器和其他回调。如果你实现了一个API——其客户端注册回调(callbacks),但是没有显式地撤销他们的注册。
除非采取一些操作来处理,否则这些回调会积累。确保回调被垃圾回收的一种方法是只存储弱引用(weak references)。
例如,仅将它们保存在 WeakHashMap 的键(key)中。
第 8 条:避免使用终结方法(finalizer)和清除方法(cleaner)
终结方法通常是不可预测的、危险的、一般情况下不必要的。
使用终结方法会导致行为不稳定、性能降低、以及可移植性问题。
在Java9中,用清除方法代替了终结方法。清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢、一般情况下也是不必要的。
Finalizer 和 Cleaner 机制的缺点:
- 不能保证他们能够及时执行。从一个对象变得不可到达开始,到它的终结方法被执行,这段时间是任意长的。
注重时间(time-critical)的任务不应该由终结方法或清除方法来完成。
永远不应该依赖终结方法或清除方法来更新重要的持久状态。例如,释放共享资源(数据库)上的锁。
- 如果忽略在终结过程中被抛出来的未捕获异常,该对象的终结过程也会终止。
未被捕获的异常会使对象处于破坏状态。(corrupt state)
- 严重的性能损失。
- 严重的安全问题。从构造器抛出的异常,应该足以防止对象继续存在;有了终结方法的存在,这一点就做不到了。
为了防止非final类受到终结方法攻击,要编写一个空的final的finalize方法。
如果类的对象中封装的资源(例如文件或线程)确实需要终止,正确做法是:
让类实现AutoCloseable,并且要求客户端在每个实例不再需要时调用close方法,利用try-with-resources确保终止,即使遇到异常也是如此。
终结方法和清除方法的合法用途:
1. 作为一个”安全网”,以防资源的拥有者忘记调用 close 方法。
虽然不能保证 Finalizer 和 Cleaner 机制会及时运行(或者是否运行),但是将资源晚一点释放也要好过永远不释放。
2. 使用Cleaner机制的方法与本地对等类(native peers)有关。
本地对等类是一个由普通对象委托的本地(非 Java)对象。
由于本地对等类不是普通的 Java 对象,所以垃圾收集器并不知道它,当它的 Java 对等对象被回收时,本地对等类也不会被回收。
假设性能是可以接受的,并且本地对等类没有持有关键的资源,那么 Finalizer 和 Cleaner 机制可能是这项任务的合适的工具。
总之,除非作为安全网或者为了终止非关键的本地资源,不要使用清除方法。对于Java9之前的版本,尽量不要使用终结方法。
第 9 条:try-with-resources优先于try-finally
Java 类库中包含许多必须手动调用 close 方法来关闭的资源, 比如InputStream、OutputStream 和 java.sql.Connection。
从以往来看,try-finally 语句是保证资源正确关闭的最佳方式,即使是在程序抛出异常或返回的情况下:
// try-finally - No longer the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
上述例子看起来还行,但是如果添加第二个资源,就会很糟:
// try-finally is ugly when used with more than one resource!
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}
例如,在 firstLineOfFile 方法中,由于底层物理设备发生故障,对 readLine 方法的调用可能会引发异常,并且由于相同的原因,调用 close 方法可能也会失败。 在这种情况下,第二个异常完全冲掉了第一个异常。 在异常堆栈跟踪中没有第一个异常的记录,这可能使实际系统中的调试变得非常复杂——通常你想要看到第一个异常来诊断问题。 虽然可以编写代码来抑制第二个异常,保留第一个异常,但是实际上没有人这样做,因为实现起来太繁琐了。
Java 7 引入了 try-with-resources 语句时,所有这些问题都得到了解决。要使用这个构造,资源必须实现 AutoCloseable 接口,该接口由一个返回类型为 void 的 close 方法组成。Java 类库和第三方类库中的许多类和接口现在都实现或继承了 AutoCloseable 接口。如果你编写的类表示必须关闭的资源,那么这个类也应该实现 AutoCloseable 接口。
实例1:
// try-with-resources - the the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
}
实例2:
// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
使用try-with-resources更简洁,更容易进行诊断。以firstLineOffFile为例,如果调用readLine和close方法都抛出异常,后一个异常会被禁止,以保留第一个异常。
实例3:
// try-with-resources with a catch clause
static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}
还可以在 try-with-resources 语句中添加 catch 子句,就像在常规的 try-finally 语句中一样。上面例子它不会抛出异常,但是如果它不能打开或读取文件,则返回默认值。
总之,在处理必须关闭的资源时,优先考虑用try-with-resources,而不是try-finally。
[Java读书笔记] Effective Java(Third Edition) 第2章 创建和销毁对象的更多相关文章
- [Java读书笔记] Effective Java(Third Edition) 第 7 章 Lambda和Stream
在Java 8中,添加了函数式接口(functional interface),Lambda表达式和方法引用(method reference),使得创建函数对象(function object)变得 ...
- [Java读书笔记] Effective Java(Third Edition) 第 4 章 类和接口
第 15 条: 使类和成员的可访问性最小化 软件设计基本原则:信息隐藏和封装. 信息隐藏可以有效解耦,使组件可以独立地开发.测试.优化.使用和修改. 经验法则:尽可能地使每个类或者成员不被外界访问 ...
- [Java读书笔记] Effective Java(Third Edition) 第 3 章 对于所有对象都通用的方法
第 10 条:覆盖equals时请遵守通用约定 在不覆盖equals方法下,类的每个实例都只与它自身相等. 类的每个实例本质上都是唯一的. 类不需要提供一个”逻辑相等(logical equality ...
- [Java读书笔记] Effective Java(Third Edition) 第 6 章 枚举和注解
Java支持两种引用类型的特殊用途的系列:一种称为枚举类型(enum type)的类和一种称为注解类型(annotation type)的接口. 第34条:用enum代替int常量 枚举是其合法值由一 ...
- [Java读书笔记] Effective Java(Third Edition) 第 5 章 泛型
第 26 条:请不要使用原生态类型 声明中具有一个或多个类型参数的类或者接口,就是泛型(generic). 例如List接口只有单个类型参数E, 表示列表的元素类型.这个接口全称List<E&g ...
- [Effective Java]第二章 创建和销毁对象
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- 《Effective Java 2nd》第2章 创建和销毁对象
目录 第1条:考虑使用静态工厂方法代替构造器 第2条:遇到多个构造器参数时考虑用构建器 第3条:用私有构造器或者枚举类型强化Singleton属性 第4条:通过私有构造器强化不可实例化的能力 第5条: ...
- effective java 第2章-创建和销毁对象 读书笔记
背景 去年就把这本javaer必读书--effective java中文版第二版 读完了,第一遍感觉比较肤浅,今年打算开始第二遍,顺便做一下笔记,后续会持续更新. 1.考虑用静态工厂方法替代构造器 优 ...
- 【读书笔记 - Effective Java】05. 避免创建不必要的对象
1. 如果对象是不可变的(immutable),它就始终可以被重用. (1) 特别是String类型的对象. String str1 = new String("str"); // ...
随机推荐
- idea自动在文件头中添加作者和创建时间
设置路径 : File -> Settings -> Editor -> File and Code Templates 定制头模板: /** * @Author: chancy * ...
- Redis安装及前后置启动
Redis简单介绍及在Linux上安装(这里测试用是版本:redis-3.0.0.tar.gz) 一:什么是Redis? redis就是C语言编写的一个高性能的键值存储(key-value)的非关系型 ...
- 2019 Petrozavodsk Winter Camp, Yandex Cup C. Diverse Singing 上下界网络流
建图一共建四层 第一层为N个歌手 第二层为{pi,li} 第三层为{si,li} 第四层为M首歌 除了S和第一层与第三层与T之间的边为[1,INF] 其他边均为[0,1] #include<bi ...
- Jmeter - 命令行参数
同步更新至个人博客:https://njlife.top/2019/07/12/Jmeter-%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0/ Jmeter ...
- SQL 查询今天、昨天、7天内、30天的数据
今天的所有数据: 昨天的所有数据: 7天内的所有数据: 30天内的所有数据: 本月的所有数据: 本年的所有数据: 查询今天是今年的第几天: select datepart(dayofyear,getD ...
- ao的mobile解决方案
http://aicdg.com/ue4-msaa-depth/ http://aicdg.com/vulkan-mass-shader-resolve/ ao两篇paper 分bake和realti ...
- FlexPaper的深入了解和应用
作者:tabb_ 零下疯度 推荐:无痕客 最近做项目需要用到flexpaper,所以想借此机会好好的研究一下. 这是官方的下载地址:http://flexpaper.devaldi.com/downl ...
- macOS上更顺手的终端
安装iTerm2.下载地址 https://iterm2.com/downloads/stable/latest 安装Nerd Fonts.下载地址 https://github.com/ryanoa ...
- Visual Stdio的使用
以下基于vs2017版本 part 1: 问题及解决 1.命令窗口一闪而过 右键项目,选择属性--连接器---系统---子系统---选择控制台. 2.修改默认启动项目 右键解决方案,选择属性,选择当前 ...
- 用php把excel数据导入数据库
PHPExcel是一个PHP类库,用来帮助我们简单.高效实现从Excel读取Excel的数据和导出数据到Excel. 先下载PHPExcel类库· 读取文件源码: <?php header(&q ...