home | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Lucene example source code file (IndexSearcher.java)

This example Lucene source code file (IndexSearcher.java) is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Java - Lucene tags/keywords

executionhelper, filter, filter, indexreader, indexsearcher, indexsearcher, io, ioexception, ioexception, override, override, sort, threading, threads, topdocs, topfielddocs, util, weight

The Lucene IndexSearcher.java source code

package org.apache.lucene.search;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.FieldSelector;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.NIOFSDirectory; // javadocs
import org.apache.lucene.util.ReaderUtil;
import org.apache.lucene.util.ThreadInterruptedException;

/** Implements search over a single IndexReader.
 *
 * <p>Applications usually need only call the inherited
 * {@link #search(Query,int)}
 * or {@link #search(Query,Filter,int)} methods. For
 * performance reasons, if your index is unchanging, you
 * should share a single IndexSearcher instance across
 * multiple searches instead of creating a new one
 * per-search.  If your index has changed and you wish to
 * see the changes reflected in searching, you should
 * use {@link IndexReader#reopen} to obtain a new reader and
 * then create a new IndexSearcher from that.  Also, for
 * low-latency turnaround it's best to use a near-real-time
 * reader ({@link IndexReader#open(IndexWriter,boolean)}).
 * Once you have a new {@link IndexReader}, it's relatively
 * cheap to create a new IndexSearcher from it.
 * 
 * <a name="thread-safety">

NOTE: {@link * IndexSearcher}</code> instances are completely * thread safe, meaning multiple threads can call any of its * methods, concurrently. If your application requires * external synchronization, you should <b>not * synchronize on the <code>IndexSearcher instance; * use your own (non-Lucene) objects instead.</p> */ public class IndexSearcher extends Searcher { IndexReader reader; private boolean closeReader; // NOTE: these members might change in incompatible ways // in the next release protected final IndexReader[] subReaders; protected final int[] docStarts; // These are only used for multi-threaded search private final ExecutorService executor; protected final IndexSearcher[] subSearchers; /** Creates a searcher searching the index in the named * directory, with readOnly=true * @param path directory where IndexReader will be opened * @throws CorruptIndexException if the index is corrupt * @throws IOException if there is a low-level IO error */ public IndexSearcher(Directory path) throws CorruptIndexException, IOException { this(IndexReader.open(path, true), true, null); } /** Creates a searcher searching the index in the named * directory. You should pass readOnly=true, since it * gives much better concurrent performance, unless you * intend to do write operations (delete documents or * change norms) with the underlying IndexReader. * @param path directory where IndexReader will be opened * @param readOnly if true, the underlying IndexReader * will be opened readOnly * @throws CorruptIndexException if the index is corrupt * @throws IOException if there is a low-level IO error */ public IndexSearcher(Directory path, boolean readOnly) throws CorruptIndexException, IOException { this(IndexReader.open(path, readOnly), true, null); } /** Creates a searcher searching the provided index. */ public IndexSearcher(IndexReader r) { this(r, false, null); } /** Runs searches for each segment separately, using the * provided ExecutorService. IndexSearcher will not * shutdown/awaitTermination this ExecutorService on * close; you must do so, eventually, on your own. NOTE: * if you are using {@link NIOFSDirectory}, do not use * the shutdownNow method of ExecutorService as this uses * Thread.interrupt under-the-hood which can silently * close file descriptors (see <a * href="https://issues.apache.org/jira/browse/LUCENE-2239">LUCENE-2239</a>). * * @lucene.experimental */ public IndexSearcher(IndexReader r, ExecutorService executor) { this(r, false, executor); } /** Expert: directly specify the reader, subReaders and * their docID starts. * * @lucene.experimental */ public IndexSearcher(IndexReader reader, IndexReader[] subReaders, int[] docStarts) { this.reader = reader; this.subReaders = subReaders; this.docStarts = docStarts; closeReader = false; executor = null; subSearchers = null; } /** Expert: directly specify the reader, subReaders and * their docID starts, and an ExecutorService. In this * case, each segment will be separately searched using the * ExecutorService. IndexSearcher will not * shutdown/awaitTermination this ExecutorService on * close; you must do so, eventually, on your own. NOTE: * if you are using {@link NIOFSDirectory}, do not use * the shutdownNow method of ExecutorService as this uses * Thread.interrupt under-the-hood which can silently * close file descriptors (see <a * href="https://issues.apache.org/jira/browse/LUCENE-2239">LUCENE-2239</a>). * * @lucene.experimental */ public IndexSearcher(IndexReader reader, IndexReader[] subReaders, int[] docStarts, ExecutorService executor) { this.reader = reader; this.subReaders = subReaders; this.docStarts = docStarts; if (executor == null) { subSearchers = null; } else { subSearchers = new IndexSearcher[subReaders.length]; for(int i=0;i<subReaders.length;i++) { subSearchers[i] = new IndexSearcher(subReaders[i]); } } closeReader = false; this.executor = executor; } private IndexSearcher(IndexReader r, boolean closeReader, ExecutorService executor) { reader = r; this.executor = executor; this.closeReader = closeReader; List<IndexReader> subReadersList = new ArrayList(); gatherSubReaders(subReadersList, reader); subReaders = subReadersList.toArray(new IndexReader[subReadersList.size()]); docStarts = new int[subReaders.length]; int maxDoc = 0; for (int i = 0; i < subReaders.length; i++) { docStarts[i] = maxDoc; maxDoc += subReaders[i].maxDoc(); } if (executor == null) { subSearchers = null; } else { subSearchers = new IndexSearcher[subReaders.length]; for (int i = 0; i < subReaders.length; i++) { subSearchers[i] = new IndexSearcher(subReaders[i]); } } } protected void gatherSubReaders(List<IndexReader> allSubReaders, IndexReader r) { ReaderUtil.gatherSubReaders(allSubReaders, r); } /** Return the {@link IndexReader} this searches. */ public IndexReader getIndexReader() { return reader; } /** Returns the atomic subReaders used by this searcher. */ public IndexReader[] getSubReaders() { return subReaders; } /** Expert: Returns one greater than the largest possible document number. * * @see org.apache.lucene.index.IndexReader#maxDoc() */ @Override public int maxDoc() { return reader.maxDoc(); } /** Returns total docFreq for this term. */ @Override public int docFreq(final Term term) throws IOException { if (executor == null) { return reader.docFreq(term); } else { final ExecutionHelper<Integer> runner = new ExecutionHelper(executor); for(int i = 0; i < subReaders.length; i++) { final IndexSearcher searchable = subSearchers[i]; runner.submit(new Callable<Integer>() { public Integer call() throws IOException { return Integer.valueOf(searchable.docFreq(term)); } }); } int docFreq = 0; for (Integer num : runner) { docFreq += num.intValue(); } return docFreq; } } /* Sugar for .getIndexReader().document(docID) */ @Override public Document doc(int docID) throws CorruptIndexException, IOException { return reader.document(docID); } /* Sugar for .getIndexReader().document(docID, fieldSelector) */ @Override public Document doc(int docID, FieldSelector fieldSelector) throws CorruptIndexException, IOException { return reader.document(docID, fieldSelector); } /** Expert: Set the Similarity implementation used by this Searcher. * * @see Similarity#setDefault(Similarity) */ @Override public void setSimilarity(Similarity similarity) { super.setSimilarity(similarity); } @Override public Similarity getSimilarity() { return super.getSimilarity(); } /** * Note that the underlying IndexReader is not closed, if * IndexSearcher was constructed with IndexSearcher(IndexReader r). * If the IndexReader was supplied implicitly by specifying a directory, then * the IndexReader is closed. */ @Override public void close() throws IOException { if (closeReader) { reader.close(); } } /** Finds the top <code>n * hits for <code>query. * * @throws BooleanQuery.TooManyClauses */ @Override public TopDocs search(Query query, int n) throws IOException { return search(query, null, n); } /** Finds the top <code>n * hits for <code>query, applying filter if non-null. * * @throws BooleanQuery.TooManyClauses */ @Override public TopDocs search(Query query, Filter filter, int n) throws IOException { return search(createNormalizedWeight(query), filter, n); } /** Lower-level search API. * * <p>{@link Collector#collect(int)} is called for every matching * document. * <br>Collector-based access to remote indexes is discouraged. * * <p>Applications should only use this if they need all of the * matching documents. The high-level search API ({@link * Searcher#search(Query, Filter, int)}) is usually more efficient, as it skips * non-high-scoring hits. * * @param query to match documents * @param filter if non-null, used to permit documents to be collected. * @param results to receive hits * @throws BooleanQuery.TooManyClauses */ @Override public void search(Query query, Filter filter, Collector results) throws IOException { search(createNormalizedWeight(query), filter, results); } /** Lower-level search API. * * <p>{@link Collector#collect(int)} is called for every matching document. * * <p>Applications should only use this if they need all of the * matching documents. The high-level search API ({@link * Searcher#search(Query, int)}) is usually more efficient, as it skips * non-high-scoring hits. * <p>Note: The score passed to this method is a raw score. * In other words, the score will not necessarily be a float whose value is * between 0 and 1. * @throws BooleanQuery.TooManyClauses */ @Override public void search(Query query, Collector results) throws IOException { search(createNormalizedWeight(query), null, results); } /** Search implementation with arbitrary sorting. Finds * the top <code>n hits for query, applying * <code>filter if non-null, and sorting the hits by the criteria in * <code>sort. * * <p>NOTE: this does not compute scores by default; use * {@link IndexSearcher#setDefaultFieldSortScoring} to * enable scoring. * * @throws BooleanQuery.TooManyClauses */ @Override public TopFieldDocs search(Query query, Filter filter, int n, Sort sort) throws IOException { return search(createNormalizedWeight(query), filter, n, sort); } /** * Search implementation with arbitrary sorting and no filter. * @param query The query to search for * @param n Return only the top n results * @param sort The {@link org.apache.lucene.search.Sort} object * @return The top docs, sorted according to the supplied {@link org.apache.lucene.search.Sort} instance * @throws IOException */ @Override public TopFieldDocs search(Query query, int n, Sort sort) throws IOException { return search(createNormalizedWeight(query), null, n, sort); } /** Expert: Low-level search implementation. Finds the top <code>n * hits for <code>query, applying filter if non-null. * * <p>Applications should usually call {@link Searcher#search(Query,int)} or * {@link Searcher#search(Query,Filter,int)} instead. * @throws BooleanQuery.TooManyClauses */ @Override public TopDocs search(Weight weight, Filter filter, int nDocs) throws IOException { if (executor == null) { // single thread int limit = reader.maxDoc(); if (limit == 0) { limit = 1; } nDocs = Math.min(nDocs, limit); TopScoreDocCollector collector = TopScoreDocCollector.create(nDocs, !weight.scoresDocsOutOfOrder()); search(weight, filter, collector); return collector.topDocs(); } else { final HitQueue hq = new HitQueue(nDocs, false); final Lock lock = new ReentrantLock(); final ExecutionHelper<TopDocs> runner = new ExecutionHelper(executor); for (int i = 0; i < subReaders.length; i++) { // search each sub runner.submit( new MultiSearcherCallableNoSort(lock, subSearchers[i], weight, filter, nDocs, hq, docStarts[i])); } int totalHits = 0; float maxScore = Float.NEGATIVE_INFINITY; for (final TopDocs topDocs : runner) { if(topDocs.totalHits != 0) { totalHits += topDocs.totalHits; maxScore = Math.max(maxScore, topDocs.getMaxScore()); } } final ScoreDoc[] scoreDocs = new ScoreDoc[hq.size()]; for (int i = hq.size() - 1; i >= 0; i--) // put docs in array scoreDocs[i] = hq.pop(); return new TopDocs(totalHits, scoreDocs, maxScore); } } /** Expert: Low-level search implementation with arbitrary sorting. Finds * the top <code>n hits for query, applying * <code>filter if non-null, and sorting the hits by the criteria in * <code>sort. * * <p>Applications should usually call {@link * Searcher#search(Query,Filter,int,Sort)} instead. * * @throws BooleanQuery.TooManyClauses */ @Override public TopFieldDocs search(Weight weight, Filter filter, final int nDocs, Sort sort) throws IOException { return search(weight, filter, nDocs, sort, true); } /** * Just like {@link #search(Weight, Filter, int, Sort)}, but you choose * whether or not the fields in the returned {@link FieldDoc} instances should * be set by specifying fillFields. * * <p>NOTE: this does not compute scores by default. If you * need scores, create a {@link TopFieldCollector} * instance by calling {@link TopFieldCollector#create} and * then pass that to {@link #search(Weight, Filter, * Collector)}.</p> */ protected TopFieldDocs search(Weight weight, Filter filter, int nDocs, Sort sort, boolean fillFields) throws IOException { if (sort == null) throw new NullPointerException(); if (executor == null) { // single thread int limit = reader.maxDoc(); if (limit == 0) { limit = 1; } nDocs = Math.min(nDocs, limit); TopFieldCollector collector = TopFieldCollector.create(sort, nDocs, fillFields, fieldSortDoTrackScores, fieldSortDoMaxScore, !weight.scoresDocsOutOfOrder()); search(weight, filter, collector); return (TopFieldDocs) collector.topDocs(); } else { final TopFieldCollector topCollector = TopFieldCollector.create(sort, nDocs, fillFields, fieldSortDoTrackScores, fieldSortDoMaxScore, false); final Lock lock = new ReentrantLock(); final ExecutionHelper<TopFieldDocs> runner = new ExecutionHelper(executor); for (int i = 0; i < subReaders.length; i++) { // search each sub runner.submit( new MultiSearcherCallableWithSort(lock, subSearchers[i], weight, filter, nDocs, topCollector, sort, docStarts[i])); } int totalHits = 0; float maxScore = Float.NEGATIVE_INFINITY; for (final TopFieldDocs topFieldDocs : runner) { if (topFieldDocs.totalHits != 0) { totalHits += topFieldDocs.totalHits; maxScore = Math.max(maxScore, topFieldDocs.getMaxScore()); } } final TopFieldDocs topDocs = (TopFieldDocs) topCollector.topDocs(); return new TopFieldDocs(totalHits, topDocs.scoreDocs, topDocs.fields, topDocs.getMaxScore()); } } /** * Lower-level search API. * * <p> * {@link Collector#collect(int)} is called for every document. <br> * Collector-based access to remote indexes is discouraged. * * <p> * Applications should only use this if they need <i>all of the matching * documents. The high-level search API ({@link Searcher#search(Query,int)}) is * usually more efficient, as it skips non-high-scoring hits. * * @param weight * to match documents * @param filter * if non-null, used to permit documents to be collected. * @param collector * to receive hits * @throws BooleanQuery.TooManyClauses */ @Override public void search(Weight weight, Filter filter, Collector collector) throws IOException { // TODO: should we make this // threaded...? the Collector could be sync'd? // always use single thread: if (filter == null) { for (int i = 0; i < subReaders.length; i++) { // search each subreader collector.setNextReader(subReaders[i], docStarts[i]); Scorer scorer = weight.scorer(subReaders[i], !collector.acceptsDocsOutOfOrder(), true); if (scorer != null) { scorer.score(collector); } } } else { for (int i = 0; i < subReaders.length; i++) { // search each subreader collector.setNextReader(subReaders[i], docStarts[i]); searchWithFilter(subReaders[i], weight, filter, collector); } } } private void searchWithFilter(IndexReader reader, Weight weight, final Filter filter, final Collector collector) throws IOException { assert filter != null; Scorer scorer = weight.scorer(reader, true, false); if (scorer == null) { return; } int docID = scorer.docID(); assert docID == -1 || docID == DocIdSetIterator.NO_MORE_DOCS; // CHECKME: use ConjunctionScorer here? DocIdSet filterDocIdSet = filter.getDocIdSet(reader); if (filterDocIdSet == null) { // this means the filter does not accept any documents. return; } DocIdSetIterator filterIter = filterDocIdSet.iterator(); if (filterIter == null) { // this means the filter does not accept any documents. return; } int filterDoc = filterIter.nextDoc(); int scorerDoc = scorer.advance(filterDoc); collector.setScorer(scorer); while (true) { if (scorerDoc == filterDoc) { // Check if scorer has exhausted, only before collecting. if (scorerDoc == DocIdSetIterator.NO_MORE_DOCS) { break; } collector.collect(scorerDoc); filterDoc = filterIter.nextDoc(); scorerDoc = scorer.advance(filterDoc); } else if (scorerDoc > filterDoc) { filterDoc = filterIter.advance(scorerDoc); } else { scorerDoc = scorer.advance(filterDoc); } } } /** Expert: called to re-write queries into primitive queries. * @throws BooleanQuery.TooManyClauses */ @Override public Query rewrite(Query original) throws IOException { Query query = original; for (Query rewrittenQuery = query.rewrite(reader); rewrittenQuery != query; rewrittenQuery = query.rewrite(reader)) { query = rewrittenQuery; } return query; } /** Returns an Explanation that describes how <code>doc scored against * <code>query. * * <p>This is intended to be used in developing Similarity implementations, * and, for good performance, should not be displayed with every hit. * Computing an explanation is as expensive as executing the query over the * entire index. */ @Override public Explanation explain(Query query, int doc) throws IOException { return explain(createNormalizedWeight(query), doc); } /** Expert: low-level implementation method * Returns an Explanation that describes how <code>doc scored against * <code>weight. * * <p>This is intended to be used in developing Similarity implementations, * and, for good performance, should not be displayed with every hit. * Computing an explanation is as expensive as executing the query over the * entire index. * <p>Applications should call {@link Searcher#explain(Query, int)}. * @throws BooleanQuery.TooManyClauses */ @Override public Explanation explain(Weight weight, int doc) throws IOException { int n = ReaderUtil.subIndex(doc, docStarts); int deBasedDoc = doc - docStarts[n]; return weight.explain(subReaders[n], deBasedDoc); } private boolean fieldSortDoTrackScores; private boolean fieldSortDoMaxScore; /** By default, no scores are computed when sorting by * field (using {@link #search(Query,Filter,int,Sort)}). * You can change that, per IndexSearcher instance, by * calling this method. Note that this will incur a CPU * cost. * * @param doTrackScores If true, then scores are * returned for every matching document in {@link * TopFieldDocs}. * * @param doMaxScore If true, then the max score for all * matching docs is computed. */ public void setDefaultFieldSortScoring(boolean doTrackScores, boolean doMaxScore) { fieldSortDoTrackScores = doTrackScores; fieldSortDoMaxScore = doMaxScore; if (subSearchers != null) { // propagate settings to subs for (IndexSearcher sub : subSearchers) { sub.setDefaultFieldSortScoring(doTrackScores, doMaxScore); } } } /** * Creates a normalized weight for a top-level {@link Query}. * The query is rewritten by this method and {@link Query#createWeight} called, * afterwards the {@link Weight} is normalized. The returned {@code Weight} * can then directly be used to get a {@link Scorer}. * @lucene.internal */ public Weight createNormalizedWeight(Query query) throws IOException { return super.createNormalizedWeight(query); } /** * A thread subclass for searching a single searchable */ private static final class MultiSearcherCallableNoSort implements Callable<TopDocs> { private final Lock lock; private final IndexSearcher searchable; private final Weight weight; private final Filter filter; private final int nDocs; private final HitQueue hq; private final int docBase; public MultiSearcherCallableNoSort(Lock lock, IndexSearcher searchable, Weight weight, Filter filter, int nDocs, HitQueue hq, int docBase) { this.lock = lock; this.searchable = searchable; this.weight = weight; this.filter = filter; this.nDocs = nDocs; this.hq = hq; this.docBase = docBase; } public TopDocs call() throws IOException { final TopDocs docs = searchable.search (weight, filter, nDocs); final ScoreDoc[] scoreDocs = docs.scoreDocs; for (int j = 0; j < scoreDocs.length; j++) { // merge scoreDocs into hq final ScoreDoc scoreDoc = scoreDocs[j]; scoreDoc.doc += docBase; // convert doc //it would be so nice if we had a thread-safe insert lock.lock(); try { if (scoreDoc == hq.insertWithOverflow(scoreDoc)) break; } finally { lock.unlock(); } } return docs; } } /** * A thread subclass for searching a single searchable */ private static final class MultiSearcherCallableWithSort implements Callable<TopFieldDocs> { private final Lock lock; private final IndexSearcher searchable; private final Weight weight; private final Filter filter; private final int nDocs; private final TopFieldCollector hq; private final int docBase; private final Sort sort; public MultiSearcherCallableWithSort(Lock lock, IndexSearcher searchable, Weight weight, Filter filter, int nDocs, TopFieldCollector hq, Sort sort, int docBase) { this.lock = lock; this.searchable = searchable; this.weight = weight; this.filter = filter; this.nDocs = nDocs; this.hq = hq; this.docBase = docBase; this.sort = sort; } private final class FakeScorer extends Scorer { float score; int doc; public FakeScorer() { super(null, null); } @Override public int advance(int target) { throw new UnsupportedOperationException(); } @Override public int docID() { return doc; } @Override public float freq() { throw new UnsupportedOperationException(); } @Override public int nextDoc() { throw new UnsupportedOperationException(); } @Override public float score() { return score; } } private final FakeScorer fakeScorer = new FakeScorer(); public TopFieldDocs call() throws IOException { final TopFieldDocs docs = searchable.search (weight, filter, nDocs, sort); // If one of the Sort fields is FIELD_DOC, need to fix its values, so that // it will break ties by doc Id properly. Otherwise, it will compare to // 'relative' doc Ids, that belong to two different searchables. for (int j = 0; j < docs.fields.length; j++) { if (docs.fields[j].getType() == SortField.DOC) { // iterate over the score docs and change their fields value for (int j2 = 0; j2 < docs.scoreDocs.length; j2++) { FieldDoc fd = (FieldDoc) docs.scoreDocs[j2]; fd.fields[j] = Integer.valueOf(((Integer) fd.fields[j]).intValue() + docBase); } break; } } lock.lock(); try { hq.setNextReader(searchable.getIndexReader(), docBase); hq.setScorer(fakeScorer); for(ScoreDoc scoreDoc : docs.scoreDocs) { fakeScorer.doc = scoreDoc.doc; fakeScorer.score = scoreDoc.score; hq.collect(scoreDoc.doc); } } finally { lock.unlock(); } return docs; } } /** * A helper class that wraps a {@link CompletionService} and provides an * iterable interface to the completed {@link Callable} instances. * * @param <T> * the type of the {@link Callable} return value */ private static final class ExecutionHelper<T> implements Iterator, Iterable { private final CompletionService<T> service; private int numTasks; ExecutionHelper(final Executor executor) { this.service = new ExecutorCompletionService<T>(executor); } public boolean hasNext() { return numTasks > 0; } public void submit(Callable<T> task) { this.service.submit(task); ++numTasks; } public T next() { if(!this.hasNext()) throw new NoSuchElementException(); try { return service.take().get(); } catch (InterruptedException e) { throw new ThreadInterruptedException(e); } catch (ExecutionException e) { throw new RuntimeException(e); } finally { --numTasks; } } public void remove() { throw new UnsupportedOperationException(); } public Iterator<T> iterator() { // use the shortcut here - this is only used in a private context return this; } } @Override public String toString() { return "IndexSearcher(" + reader + ")"; } }

Other Lucene examples (source code examples)

Here is a short list of links related to this Lucene IndexSearcher.java source code file:



my book on functional programming

 

new blog posts

 

Copyright 1998-2019 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.