Lazy streams and their terminal operations.

Okay, I have been bending over backwords trying to use streams inappropriately. So I found this issue about using DoubleStream#max and DoubleStream#min, they are both terminal operations.
For example lets say we have a bunch of Rectangle2D’s and we want to make a histogram of the widths. To do this we have to find the min, the max, create some bins, and iterate over the rectangles to place each one in its respective bin.
import javafx.geometry.Rectangle2D;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
/**
 * Created by odinsbane on 3/25/15.
 */
public class StreamCheck {
    /**
     * Broken version of making a histogram of rectangle widths.
     * @param rectangles collection of rectangles that will be histogrammed.
     * @return a list of bin, count pairs.
     */
    static List<double[]> broken(List<Rectangle2D> rectangles){
        double[] extrema = new double[]{-Double.MAX_VALUE, Double.MAX_VALUE};
        //since I cannot use both widths.max() and widths.min(), I tried to get
        //these values on the mapToDouble loop.
        DoubleStream widths = rectangles.stream().mapToDouble((rect)->{
            double w = rect.getWidth();
            extrema[0] = w>extrema[0]?w:extrema[0];
            extrema[1] = w<extrema[1]?w:extrema[1];
            return w;
        });
        /*
         * This is broken, because min and max have not been set yet.
         */
        int bins = 20;
        double min = extrema[1];
        double max = extrema[0];
        double delta = (max - min)/bins;
        System.out.printf("before %2.2f\t%2.2f\n",extrema[0], extrema[1]);
        List<double[]> op = IntStream.range(0, bins).mapToObj(
                (i)->new double[]{(i+0.5)*delta + min, 0}
        ).collect(Collectors.toCollection(ArrayList::new));
        //put the data in the bins.
        widths.forEach((d)->{
            int dex = (int)((d - min)/delta);
            dex = dex==op.size()?dex-1:dex;
            op.get(dex)[1] = op.get(dex)[1]+1;
        });
        System.out.printf("after %2.2f\t%2.2f\n", extrema[0], extrema[1]);
        return op;
    }
    /**
     * Fixed by using a for each to make the histogram of rectangle widths.
     * @param rectangles
     * @return a list of bin, count pairs.
     */
    static List<double[]> fixed(List<Rectangle2D> rectangles){
        //input is List<Rectangle2D> rectangles.
        double[] extrema = new double[]{-Double.MAX_VALUE, Double.MAX_VALUE};
        rectangles.stream().forEach((rect)->{
            double w = rect.getWidth();
            extrema[0] = w>extrema[0]?w:extrema[0];
            extrema[1] = w<extrema[1]?w:extrema[1];
        });
        DoubleStream widths = rectangles.stream().mapToDouble(Rectangle2D::getWidth);
        int bins = 20;
        double min = extrema[1];
        double max = extrema[0];
        double delta = (max - min)/bins;
        System.out.printf("before %2.2f\t%2.2f\n",extrema[0], extrema[1]);
        List<double[]> op = IntStream.range(0, bins).mapToObj(
                (i)->new double[]{(i+0.5)*delta + min, 0}
        ).collect(Collectors.toCollection(ArrayList::new));
        //put the data in the bins.
        widths.forEach((d)->{
            int dex = (int)((d - min)/delta);
            dex = dex==op.size()?dex-1:dex;
            op.get(dex)[1] = op.get(dex)[1]+1;
        });
        System.out.printf("after %2.2f\t%2.2f\n", extrema[0], extrema[1]);
        return op;
    }
    static Rectangle2D random(){
        return new Rectangle2D(Math.random(), Math.random(), Math.random(), Math.random());
    }
    public static void main(String[] args){
        List<Rectangle2D> rectangles = new ArrayList<>();
        IntStream.range(0,100).forEach((i)->rectangles.add(random()));
        broken(rectangles);
        fixed(rectangles);
    }
}

Since mapToDouble is an intermediate operation it happens lazily and does not get executed until the .forEach is called later on. So I had to use a terminal operation, in the fixed version a forEach is used.

Uncle Bob: "Make the Magic go away"

Bob Martin, also known in the industry as “Uncle Bob,” wrote a blog post entitled “Make the Magic go away,” which focuses mainly on whether you need a framework, and some minor thoughts about how you might judge.
It’s an interesting article. It uses rxJava as a launching point for thought, using Bob’s own history in the industry as observation (of the Observation Pattern, of all things).
There are some lightning-rod statements:

The authors of rxJava, and of Spring, and JSF, and JPA, and Struts, and [put your favorite framework here] are all searching for the same thing. These frameworks are born out of frustration with the language; and are an attempt to improve upon that language.
Every framework you’ve ever seen is really just an echo of this statement:
My language sucks!
And so we write frameworks to compensate for the lack of features that we wish were in our language.

It’s fair to say that this is hyperbole (and Bob knows it) – a point exaggerated

Using packr to bundle an OS X JRE, from linux

(by Eron Gjoni, rufsketch1#gmail.com)
packr is a useful little program that allows you to include a copy of the JRE along with your runnable JAR file. Making it much easier for your users to just download and run your program, without them having to worry about java versions or installation procedures. You can learn to use packr from the documentations available on its github page
https://github.com/libgdx/packr
Notably, however, packr requires platform specific copies of the JDK in order to bundle a JRE with your program. This is fine if you have easy access to each of the platforms you wish to support, or if you intend to use openJDK. But what do you do if you only have linux, but want to create a self contained runnable for OS X users? Oracle only provides the OSX JDK as a dmg. Luckily, we can still access the files we need!
Download a copy of the Java SE JDK for OS X from Oracle.
use 7zip to extract the contents of the .dmg.

7z x jdx-[version number]-x64.dmg -oosxJDK

cd into the osxJDK subdirectory created by the command above.
You’ll see a number of files that were extracted from the dmg.
We’re interested in the contents of the file ending in .hfs.
To access its contents, first make sure you have the hfsplus module active.

sudo modprobe hfsplus

Then, create a subdirectory into which to mount the .hfs file.

mkdir mountedHFS

Then, mount the file

mount -oloop 4.hfs mountedHFS

If you cd into the mountedHFS directory, you will find a .pkg file. Move it to the parent directory and then unmount the .hfs file.

sudo mv JDK\ 8\ Update\ 131.pkg ../ && cd ../ && sudo umount mountedHFS

Next, extract the contents of the .pkg file into a new subdirectory.

7z x JDK\ 8\ Update\ 131.pkg -opkg

cd into that subdirectory

cd pkg

You will find a number of files and subdirectories. cd into the jdk180131.pkg subdirectory (replacing the version number as appropriate)

cd jdk180131.pkg

There you will find a file named Payload. We will need to extract the contents of this file using

cat Payload | gunzip -dc |cpio -i

A new subdirectory called Contents will be created. if you cd into Contents/Home you will find the files needed by packr to create a self-contained bundle for OSX.

Symptoms and Solutions

I casually watch a lot of forums related to Java: IRC, of course, and Discord, and Slack, and Reddit. On Reddit, on r/java, there’re some occasionally quite interesting projects offered for consideration; they’re usually pet projects for the authors, and that’s healthy and useful for everyone.

However, there’s also a low hum of … tooling. Two really interesting projects showed up recently, for example, that avoided standard tooling: one used PowerShell, another used a bash script to build.

Both of these projects work. They both generated the expected output; the bash script actually went way above and beyond (replicating what a good Gradle script might do), downloading a JVM and manually grabbing the dependencies via wget, then manually calling javac and jlink in a manner that would make Mark Reinhold proud, then building an AppImage for x86_64 using the platform-appropriate tools.

The Reddit discussion on that latter one did not go especially well, however, because the tooling discussion ended up front and center, with what looks like a lot of hurt feelings.

“This looks like a novice project” was the import of one comment, with another being that the tooling might have helped; that went south quickly. It was not a “novice project.” The author was solving a problem they encountered in their own way, including implementing some X11 protocols in Java. Project infrastructure choices don’t make one a “novice,” especially as an insult.

The thing is, the comment about tooling might be pretty apt. My personal thought about the project, based on the discussion, was “why not tooling?” Are the tools insufficient? The project owner built an x86_64 image specifically; that may suit their purposes (and as the primary project consumer, their purposes are most important) but what would it have taken to build an ARM image? Would the tooling have been sufficient for that?

It might not have been. The way I see it, there’re a few reasons the tooling would be avoided:

  • Lack of awareness. The author said they knew about the tooling and avoided it, but maybe they didn’t check to see if the tooling could create AppImages.
  • Lack of functionality. Maybe they did check, but the functionality wasn’t there, or wasn’t sufficient.
  • Lack of documentation. Maybe the functionality was there, but was documented poorly enough that it was easier to do it the straightforward way instead. (Is that OSGi I hear weeping in the distance? It just might be.)
  • Personal preference. Maybe the OP knew about the tooling and just didn’t care.

If personal preference is involved, well, the discussion’s over. At that point, it’s like arguing that the author’s favorite number should clearly be 18 instead of 11. It’s their project, not anyone else’s, and if they make a decision based on their preference, that’s their right and power.

But the other options, well, those are things that we as a community might be able to help. And I think we should consider helping, even if the “help” might not apply to this specific circumstance.

So: We can help, but we need to consider:

  • How we approach the help
  • What use cases are poorly served by the tooling today
  • How are we evangelizing our practices, and why
  • What would “better” look like?
  • Do we even understand each other to understand why people choose different approaches?

These aren’t necessarily idle musings. We want Java to remain vital, we want to do what we can to establish common practices so we can help each other, we want to make sure that we’re not presenting barriers to people whose skills we want in our ecosystem; it’d be nice if we could offer commentary without sneering (and, to be fair, accept commentary without bristling.)

Let’s all be kind to each other, an effort that would make us more effective as well.

Locale-specific numbers

One of our channel members mentioned a failure in parsing “-1” to an int given a Locale. That’s … fascinating, actually, as I (dreamreal) was unaware of any numbering systems under which that would fail. So, as any programmer would, I wanted a program to show me the locales for which “-1” wasn’t the proper representation for “negative one.”

Being a programmer, I … immediately ran to an AI (Claude, specifically) and had it generate a table of the possibilities, along with the associated locales, that did not fit the “normal” representation of “-1” – where, well, my locale (US English) was “normal.”

If you’re not English, please recognize the humor here – I’m well aware that Urdu readers would think their representation was “normal” and “-1” was not. Or, well, I am aware now and wasn’t before, and my use of “normal” is entirely meant to poke fun at my own English-centric expectations, because it didn’t occur to me that negatives using Arabic sigils might not be the same everywhere.

Anyway, this is what came out of it, for my Java 25 installation. The table shows the representation (the best that I could get WordPress to trivially display it, at least), the Unicode transcription, and then a list of the locales that emitted that particular representation. I didn’t bother including “-1” because, well, it’s a ginormous list and this table is long enough already.

Representation Character Analysis Locales (168 total)
-? len:2, chars:U+002D,U+07C1 nqo (N’Ko), nqo_GN (N’Ko (Guinea)), nqo_GN_#Nkoo (N’Ko (N’Ko, Guinea))
-? len:2, chars:U+002D,U+0967 bgc (Haryanvi), bgc_IN (Haryanvi (India)), bgc_IN_#Deva (Haryanvi (Devanagari, India)), bho (Bhojpuri), bho_IN (Bhojpuri (India)), bho_IN_#Deva (Bhojpuri (Devanagari, India)), mr (Marathi), mr_IN (Marathi (India)), mr_IN_#Deva (Marathi (Devanagari, India)), ne (Nepali), ne_IN (Nepali (India)), ne_NP (Nepali (Nepal)), ne_NP_#Deva (Nepali (Devanagari, Nepal)), raj (Rajasthani), raj_IN (Rajasthani (India)), raj_IN_#Deva (Rajasthani (Devanagari, India)), sa (Sanskrit), sa_IN (Sanskrit (India)), sa_IN_#Deva (Sanskrit (Devanagari, India))
-? len:2, chars:U+002D,U+09E7 as (Assamese), as_IN (Assamese (India)), as_IN_#Beng (Assamese (Bangla, India)), bn (Bangla), bn_BD (Bangla (Bangladesh)), bn_BD_#Beng (Bangla (Bangla, Bangladesh)), bn_IN (Bangla (India)), mni (Manipuri), mni_IN (Manipuri (India)), mni_IN_#Beng (Manipuri (Bangla, India)), mni__#Beng (Manipuri (Bangla))
-? len:2, chars:U+002D,U+0E51 th_TH_TH_#u-nu-thai (Thai (Thailand, TH, Thai Digits))
-? len:2, chars:U+002D,U+0F21 dz (Dzongkha), dz_BT (Dzongkha (Bhutan)), dz_BT_#Tibt (Dzongkha (Tibetan, Bhutan))
-? len:2, chars:U+002D,U+1041 my (Burmese), my_MM (Burmese (Myanmar (Burma))), my_MM_#Mymr (Burmese (Myanmar, Myanmar (Burma)))
-? len:2, chars:U+002D,U+1C51 sat (Santali), sat_IN (Santali (India)), sat_IN_#Olck (Santali (Ol Chiki, India)), sat__#Olck (Santali (Ol Chiki))
-? len:3, chars:U+061C,U+002D,U+0661 ar_BH (Arabic (Bahrain)), ar_DJ (Arabic (Djibouti)), ar_EG (Arabic (Egypt)), ar_EG_#Arab (Arabic (Arabic, Egypt)), ar_ER (Arabic (Eritrea)), ar_IL (Arabic (Israel)), ar_IQ (Arabic (Iraq)), ar_JO (Arabic (Jordan)), ar_KM (Arabic (Comoros)), ar_KW (Arabic (Kuwait)), ar_LB (Arabic (Lebanon)), ar_MR (Arabic (Mauritania)), ar_OM (Arabic (Oman)), ar_PS (Arabic (Palestinian Territories)), ar_QA (Arabic (Qatar)), ar_SA (Arabic (Saudi Arabia)), ar_SD (Arabic (Sudan)), ar_SO (Arabic (Somalia)), ar_SS (Arabic (South Sudan)), ar_SY (Arabic (Syria)), ar_TD (Arabic (Chad)), ar_YE (Arabic (Yemen)), sd (Sindhi), sd_IN (Sindhi (India)), sd_PK (Sindhi (Pakistan)), sd_PK_#Arab (Sindhi (Arabic, Pakistan)), sd__#Arab (Sindhi (Arabic))
-1 len:3, chars:U+200E,U+002D,U+0031 ar (Arabic), ar_001 (Arabic (world)), ar_AE (Arabic (United Arab Emirates)), ar_DZ (Arabic (Algeria)), ar_EH (Arabic (Western Sahara)), ar_LY (Arabic (Libya)), ar_MA (Arabic (Morocco)), ar_TN (Arabic (Tunisia)), he (Hebrew), he_IL (Hebrew (Israel)), he_IL_#Hebr (Hebrew (Hebrew, Israel)), ur (Urdu), ur_PK (Urdu (Pakistan)), ur_PK_#Arab (Urdu (Arabic, Pakistan))
-? len:4, chars:U+200E,U+002D,U+200E,U+06F1 ks (Kashmiri), ks_IN (Kashmiri (India)), ks_IN_#Arab (Kashmiri (Arabic, India)), ks__#Arab (Kashmiri (Arabic)), lrc (Northern Luri), lrc_IQ (Northern Luri (Iraq)), lrc_IR (Northern Luri (Iran)), lrc_IR_#Arab (Northern Luri (Arabic, Iran)), mzn (Mazanderani), mzn_IR (Mazanderani (Iran)), mzn_IR_#Arab (Mazanderani (Arabic, Iran)), pa_PK_#Arab (Punjabi (Arabic, Pakistan)), pa__#Arab (Punjabi (Arabic)), ps (Pashto), ps_AF (Pashto (Afghanistan)), ps_AF_#Arab (Pashto (Arabic, Afghanistan)), ps_PK (Pashto (Pakistan)), ur_IN (Urdu (India)), uz_AF_#Arab (Uzbek (Arabic, Afghanistan)), uz__#Arab (Uzbek (Arabic))
?? len:3, chars:U+200E,U+2212,U+06F1 fa (Persian), fa_AF (Persian (Afghanistan)), fa_IR (Persian (Iran)), fa_IR_#Arab (Persian (Arabic, Iran))
-? len:3, chars:U+200F,U+002D,U+0661 ckb (Central Kurdish), ckb_IQ (Central Kurdish (Iraq)), ckb_IQ_#Arab (Central Kurdish (Arabic, Iraq)), ckb_IR (Central Kurdish (Iran))
?1 len:2, chars:U+2212,U+0031 et (Estonian), et_EE (Estonian (Estonia)), et_EE_#Latn (Estonian (Latin, Estonia)), eu (Basque), eu_ES (Basque (Spain)), eu_ES_#Latn (Basque (Latin, Spain)), fi (Finnish), fi_FI (Finnish (Finland)), fi_FI_#Latn (Finnish (Latin, Finland)), fo (Faroese), fo_DK (Faroese (Denmark)), fo_FO (Faroese (Faroe Islands)), fo_FO_#Latn (Faroese (Latin, Faroe Islands)), gsw (Swiss German), gsw_CH (Swiss German (Switzerland)), gsw_CH_#Latn (Swiss German (Latin, Switzerland)), gsw_FR (Swiss German (France)), gsw_LI (Swiss German (Liechtenstein)), hr (Croatian), hr_BA (Croatian (Bosnia & Herzegovina)), hr_HR (Croatian (Croatia)), hr_HR_#Latn (Croatian (Latin, Croatia)), ksh (Colognian), ksh_DE (Colognian (Germany)), ksh_DE_#Latn (Colognian (Latin, Germany)), lt (Lithuanian), lt_LT (Lithuanian (Lithuania)), lt_LT_#Latn (Lithuanian (Latin, Lithuania)), nb (Norwegian Bokmål), nb_NO (Norwegian Bokmål (Norway)), nb_NO_#Latn (Norwegian Bokmål (Latin, Norway)), nb_SJ (Norwegian Bokmål (Svalbard & Jan Mayen)), nn (Norwegian Nynorsk), nn_NO (Norwegian Nynorsk (Norway)), nn_NO_#Latn (Norwegian Nynorsk (Latin, Norway)), no (Norwegian), no_NO (Norwegian (Norway)), no_NO_#Latn (Norwegian (Latin, Norway)), no_NO_NY (Norwegian (Norway, Nynorsk)), rm (Romansh), rm_CH (Romansh (Switzerland)), rm_CH_#Latn (Romansh (Latin, Switzerland)), se (Northern Sami), se_FI (Northern Sami (Finland)), se_NO (Northern Sami (Norway)), se_NO_#Latn (Northern Sami (Latin, Norway)), se_SE (Northern Sami (Sweden)), sl (Slovenian), sl_SI (Slovenian (Slovenia)), sl_SI_#Latn (Slovenian (Latin, Slovenia)), sv (Swedish), sv_AX (Swedish (Åland Islands)), sv_FI (Swedish (Finland)), sv_SE (Swedish (Sweden)), sv_SE_#Latn (Swedish (Latin, Sweden))

List.remove() oddities

From reddit’s r/java, a user observed that List.remove(Object) removes the first object whose equals() method returns true. Thus, if two objects have object equality with each other, but you wish to remove the second, you’re going to have to find it and return it with the indexed version of remove(int) instead of the remove(Object) call.

yawkat observed that you can use Collection.removeIf() (which List inherits) and get it done as well:

list.removeIf(o -> o == objectToRemove);

It’d probably still be better to fix equals() so it was more specific, but sometimes your code doesn’t always fit the circumstances you want.

Calling a Superclass’ Method

A user on #java asked how one might invoke a method of a superclass recently, and received some general answers that more or less were right, but weren’t technically correct.

Maldivia pointed out that you would use method handles, which is sort of the gist of the (incorrect) answers given. The attempt was made to invoke the method via getDeclaredMethod(), which does not work.

The way to do it is to use MethodHandles.lookup() to get, well, a MethodHandles.Lookup object, which provides a way to get methods in the context of a class.

First, you’d get a MethodType reference, that refers to the _return value_ of the method. Then, you’d use findSpecial() – as one likely possibility – to find a method by name in a given class.

With that, you’d be able to invoke() the method.

Here’s code showing a superclass – Drink – and a main() in a subclass, Coffee, that has a bad call (the suggestion from the channel, tried and failing) and a good call, based on Maldivia’s suggestion, that actually does invoke Drink.toString() even when called from the context of a Coffee instance.

This is not expected to be held as an example of “what you should do” – for one thing, calling superclass’ methods outside of explicit access from a subclass is terrible design, in most cases, and for another, this is an example designed to run and not run properly or exhaustively. It’s simply a starting point.

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

class Drink {
    public String toString() {
        return "This is a Drink!";
    }

}

public class Coffee extends Drink {
    public String toString() {
        return "This is a Coffee!";
    }

    static void badCall(Coffee coffee) throws Throwable {
        var method = coffee
                .getClass()
                .getSuperclass()
                .getDeclaredMethod("toString", null);
        System.out.println(method);
        System.out.println(method.invoke(coffee, null));
        System.out.println(coffee);
    }

    static void goodCall(Coffee coffee) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType type = MethodType.methodType(String.class);
        var m = lookup.findSpecial(coffee
                        .getClass()
                        .getSuperclass(),
                "toString",
                type,
                coffee.getClass()
        );
        System.out.println(m);
        System.out.println(m.invoke(coffee));
    }

    public static void main(String[] args) throws Throwable {
        Coffee coffee = new Coffee();
        badCall(coffee);
        goodCall(coffee);
    }
}

Java Multiline Properties

A channel member asked if there was a way to do multiline properties in Java, and if so, how was leading whitespace handled?

The answers are, respectively, “yes,” and “pretty much as you’d hope it was.”

The multiline character for Java properties is, as one might expect from every other programming language’s usage, the \ character, and the Properties.load() method will trim all leading whitespace from the property value. Here’s a sample:

// This is Example.java
package example;

class Example {
  public static void main(String[] args) {
// sorry for the gross formatting here, page width! try(var in=Example
.class
.getResourceAsStream("/foo.properties")) { var properties=new Properties(); properties.load(in); System.out.println(properties.getProperty("foo")); } } }

And the properties file, foo.properties, which should be visible in the classpath at the root:

# this is /foo.properties
# note how "multiline" is indented more than the rest
foo = this \
  is \
  a \
    multiline \
  property

If this is run and the classpath is set properly, the output should be this is a multiline property. You can embed newlines with \n as desired as well, if you need those in the output.

We’ve officially moved to Libera.chat.

It’s official: ##java has moved to libera.chat, and along the way changed its name from ##java to #java.

We tried not to do this, we really did. But with a gap in trust between IRC channels and IRC staff, it was bound to happen.

So what did happen?

Well, we’ve been trying to refer people to the channel blog (this site!) regularly so they could keep up with what was going on, on a
fairly regular basis. We had already moved the bot to the Libera.chat
network, because of the gap in trust, but we tried to not violate the
intent on Freenode to directly advertise a “competing network.” But these notices were fairly rare, maybe five a day.

But around the time of one of the notices, a user pointed out the migration fairly explicitly, in support of another user’s question. This was entirely innocent of ill will, mind: one user on the channel supporting another.

A few hours later, one of the IRC staff changed the channel topic and removed access for the entire set of ops, claiming the channel was abandoned and had no controls in place. The ops who were in the channel (and watching, I might add) immediately protested, because this was an entirely unwarranted action almost literally out of nowhere.

To the staff’s “credit,” they offered to restore access once the ops protested the incursion. But… remember that gap in trust?

That gap in trust was because the ops were concerned that an arbitrary decision might be made concerning the channel, and that’s exactly what happened. The staff even used the innocent user supporting another user, as part of an active conversation, as justification. “This is spam!,” was the claim, “and the ops aren’t asserting control!” — and neither claim was true.

We now have access back for ##java on Freenode. However, we’ve set the channel to be moderated (i.e., +m), to prevent possible spam; if you want to be able to talk in ##java on Freenode, you’ll need to message an op to get it.

But the channel has moved to Libera.chat (again, see ##java), along with the most invested and active of its users, as well as the bot. If you want what Freenode ##java used to be, you should migrate.

(And if you really want to, you can still join ##java – it’ll redirect to #java.)

Announcement about Migration Away From Freenode

This is an account from one op, based on casual observation. It is offered without lots of logs, and hopefully without emotion; your mileage and understanding may differ, but this view seems to be pretty consistent with what we know, so chances are it’s close to true if not factually true.

So what happened to Freenode to make us consider migration?

Basically, an ircop sold the domain for freenode to a VPN provider, who then added ads to the website against the staff policy, and then…

The VPN provider (Andrew Lee) claimed that he owned Freenode itself (as opposed to just the domain) and threatened to sue the staff (which, as a set of volunteers, didn’t have money or organization to fight this) so he basically horned his way in… and the staff quit.

So he replaced them with 4chan ops, and changed a lot of longstanding freenode policies along the way. Among them were considerations for speech considered harmful (such as hate speech towards religions or specific people groups or other protected classes.) 4chan isn’t exactly a haven for values that Freenode ##java considers important; choosing stewards from 4chan doesn’t inspire confidence.

Here’s a tweet that mentions some of this. Also see the “WTF FAQ,” a page kept up by an op on a similar programming channel.

It’s fair to argue that free speech includes speech that some consider aggressive, but Freenode policy was made to reduce social friction, and had done so fairly successfully. Changing those rules without specific stimuli feels like a harbinger of a migration to more harmful practices, an IRC server where channels are subject to behavior against which there is no official defense at the network level.

Thus, Freenode’s core values are changing, literally, and that’s not necessarily a desired condition.

The new owner has done very little to engender trust during the transition, including what started the transition. When you start off with “Hey, I own the domain, so that means I own the service, right?,” that sends all kinds of messages to the community that you don’t want the community to hear.

What’s more, the new staff is incredibly defensive about owning the network, to the point where they’re forcibly taking over channels that don’t appreciate the possibility of being taken over. This hasn’t affected ##java or the directly-associated channels yet, but it has affected some of the channels that are implicitly associated.

So What Are We Gonna Do About It?

Well… we’re going to move the channel to a new network. We never really had any other recourse outside of the good graces and intent of Freenode staff, and that has always worked, because everyone’s been pretty benevolent. But with a wholesale change in staff, it’s hard to predict what the new staff’s intent is or how responsive they’ll be, and what we do know indicates a lot of defensive posturing and not a lot of effort to engender trust.

What we‘re doing is “moving away from Freenode,” to an alternative server. There are a few main candidates: EFNet (which has a #java already), Undernet (which … also has a #java), DALNet (which not only already has a #java, but underwent its own political chaos a few years ago and is under autocratic control), OFTC (which hosts the OpenJDK channel, among many others), and Libera.chat, which was created largely by the outgoing Freenode staff as a response to Andrew Lee taking over.

Of these, the latter seems the most palatable. So that’s literally what we’re doing: we’re moving to a new server.

This is not an act of defiance or even active defense; it’s just a move that seems wisest based on the level of trust we have for the new staff and organization behind Freenode. It’s done mostly to protect the idea behind ##java and its associated channels, and it’s done without malice.

We’re going to maintain a presence on Freenode as long as we’re able to, but the “main effort” for community development will be on the new server.