com4j provides a helper class ComObjectCollector
. It is used to track and dispose of all COM objects created within a sequence of operations. For example:
ComObjectCollector objects = new ComObjectCollector();
COM4J.addListener(objects);
_Application xl = excel.ClassFactory.createApplication();
Workbooks workbooks = xl.getWorkbooks();
_Workbook workbook = workbooks.add(null, 0);
Com4jObject sheetObj = workbook.getWorksheets(1);
_Worksheet sheet = sheetObj.queryInterface(_Worksheet.class);
// ...
objects.disposeAll();
// BUG: "sheet" should be disposed at this point, but is not
COM4J.removeListener(objects);
Unfortunately, version com4j-20120426-2 leaks a COM reference in the exhibited code. Specifically, the reference held by sheet
is not disposed by objects.disposeAll.
The reason is subtle. ComObjectCollector
internally uses a WeakHashMap
to hold the object references. The references are represented by the com4j Wrapper
class. Wrapper
redefines the equals
method to account for aliased references. It turns out that this method considers sheetObj
and sheet
to be equal even though they are actually distinct references that need to be disposed individually. sheet
is therefore not actually added to the map because of that equality. Consequently, it is not disposed by objects.dispoaseAll()
.
Incidentally, Wrapper
should not be used with WeakHashMap
since the latter stipulates that it should only be used with objects that implement identity equality.
The fix to the leak is actually quite simple... re-implement the trivial class ComObjectCollector. Here is an inline version:
final Map _objects = new IdentityHashMap<>();
ComObjectListener listener = new ComObjectListener() {
@Override
public void onNewObject(Com4jObject obj) {
_objects.put(obj, null);
}
};
COM4J.addListener(listener);
_Application xl = excel.ClassFactory.createApplication();
Workbooks workbooks = xl.getWorkbooks();
_Workbook workbook = workbooks.add(null, 0);
Com4jObject sheetObj = workbook.getWorksheets(1);
_Worksheet sheet = sheetObj.queryInterface(_Worksheet.class);
// ...
for (Com4jObject obj : _objects.keySet()) {
obj.dispose();
}
COM4J.removeListener(listener);
This version uses strong references instead of weak references, but that should not be a problem in the absence of large numbers of transient references. A more elaborate implementation would have to either change the equality semantics of Wrapper
(requiring an impact analysis), or hold more direct references to the underlying COM objects in the WeakHashMap
.